From 6724d7f9e428c5105db13a36abca70ff39fd7972 Mon Sep 17 00:00:00 2001 From: Hritika Date: Wed, 14 Oct 2026 19:15:48 +0530 Subject: [PATCH 1/7] docs(android): document ringing through the Calls SDK and Push Notifications SDK Android calling no longer signals through the Chat SDK. The Calls SDK 5.1 creates, joins and ends calls; the Push Notifications SDK 2.0 calls module rings the device in every app state; the UI Kit hosts the screens. Calls SDK - Rewrite ringing.mdx for the Calls SDK-only flow (initiateCall, prepared session events, IncomingCallListener, answer/decline/cancel/end, group calls) - Rewrite voip-calling.mdx as "Push Ringing" (PNConfiguration, FCM service, answer activity, readiness, busy, missed calls, own ring screen, MANUAL) - Add migrating-from-legacy-ringing.mdx with the method-by-method mapping - setup: 5.1 version, host overrides, push dependencies, one-shot init - events: getActiveSession, onParticipantConnected, incoming-call and login listeners; actions/overview/custom-control-panel: leave(); migration guide: 5.0 to 5.1 section; troubleshooting rows UI Kit - Rewrite calling-integration.mdx; add ongoing-call.mdx (answer activity) - call-features, incoming-call (deprecated), outgoing-call, call-buttons, call-logs updated for the new flow - getting-started, methods, sound-manager, troubleshooting, llms, upgrading and color-resources edits (no extra dependency, logout, ring-screen colours) Notifications: rewrite android-push-notifications.mdx for Push SDK 2.0. Chat SDK v5: deprecation notes on calling-overview and the Call Listener. docs.json: two new nav entries. Versions are the planned public releases (calls 5.1.0, push 2.0.0, UI Kit 6.1.0); hold the merge until they are published. Co-Authored-By: Claude Fable 5.1 --- calls/android/actions.mdx | 10 +- calls/android/custom-control-panel.mdx | 8 +- calls/android/events.mdx | 122 +- .../android/migrating-from-legacy-ringing.mdx | 313 +++ calls/android/migration-guide-v5.mdx | 20 +- calls/android/overview.mdx | 8 +- calls/android/ringing.mdx | 757 +++--- calls/android/setup.mdx | 52 +- calls/android/troubleshooting.mdx | 7 +- calls/android/voip-calling.mdx | 2317 +++-------------- docs.json | 4 +- notifications/android-push-notifications.mdx | 671 ++--- sdk/android/v5/calling-overview.mdx | 14 +- sdk/android/v5/real-time-listeners.mdx | 4 + ui-kit/android/call-buttons.mdx | 59 +- ui-kit/android/call-features.mdx | 180 +- ui-kit/android/call-logs.mdx | 48 +- ui-kit/android/calling-integration.mdx | 332 ++- ui-kit/android/color-resources.mdx | 17 + ui-kit/android/getting-started-jetpack.mdx | 27 +- ui-kit/android/getting-started-kotlin.mdx | 27 +- ui-kit/android/incoming-call.mdx | 134 +- ui-kit/android/llms-android-v6.mdx | 6 +- ui-kit/android/methods.mdx | 9 +- ui-kit/android/ongoing-call.mdx | 328 +++ ui-kit/android/outgoing-call.mdx | 83 +- ui-kit/android/sound-manager.mdx | 6 +- ui-kit/android/troubleshooting.mdx | 25 +- ui-kit/android/upgrading-from-v5.mdx | 16 + 29 files changed, 2372 insertions(+), 3232 deletions(-) create mode 100644 calls/android/migrating-from-legacy-ringing.mdx create mode 100644 ui-kit/android/ongoing-call.mdx diff --git a/calls/android/actions.mdx b/calls/android/actions.mdx index f175a3e49..736aadddc 100644 --- a/calls/android/actions.mdx +++ b/calls/android/actions.mdx @@ -394,21 +394,25 @@ boolean isActive = callSession.isSessionActive(); ### Leave Session -Ends your participation and disconnects gracefully. The call continues for other participants. +Ends your participation and disconnects gracefully. In a 1:1 call this is the hang-up: the other side receives `onParticipantLeft` and the backend records the end. In a group call the session continues for the other participants. ```kotlin -callSession.leaveSession() +callSession.leave() ``` ```java -callSession.leaveSession(); +callSession.leave(); ``` + +`leaveSession()` is deprecated since 5.1 and only calls `leave()`. To end a call that is still ringing use `CometChatCalls.endCall(sessionId, status, listener)` with `cancelled` (caller) as described in [Ringing](/calls/android/ringing); `leave()` alone does not tell the other side why. + + ### Raise Hand Shows a hand-raised indicator to get attention from other participants. diff --git a/calls/android/custom-control-panel.mdx b/calls/android/custom-control-panel.mdx index 17efa4832..52845e6c8 100644 --- a/calls/android/custom-control-panel.mdx +++ b/calls/android/custom-control-panel.mdx @@ -206,7 +206,7 @@ class CallActivity : AppCompatActivity() { } btnEndCall.setOnClickListener { - callSession.leaveSession() + callSession.leave() finish() } } @@ -261,7 +261,7 @@ public class CallActivity extends AppCompatActivity { btnSwitchCamera.setOnClickListener(v -> callSession.switchCamera()); btnEndCall.setOnClickListener(v -> { - callSession.leaveSession(); + callSession.leave(); finish(); }); } @@ -470,7 +470,7 @@ class CallActivity : AppCompatActivity() { } btnEndCall.setOnClickListener { - callSession.leaveSession() + callSession.leave() finish() } } @@ -601,7 +601,7 @@ public class CallActivity extends AppCompatActivity { btnSwitchCamera.setOnClickListener(v -> callSession.switchCamera()); btnEndCall.setOnClickListener(v -> { - callSession.leaveSession(); + callSession.leave(); finish(); }); } diff --git a/calls/android/events.mdx b/calls/android/events.mdx index 9d8ac39a3..93e735db7 100644 --- a/calls/android/events.mdx +++ b/calls/android/events.mdx @@ -5,7 +5,7 @@ sdk_version: "5.x" description: "Handle CometChat Calls SDK v5 events on Android for call lifecycle, participants, media state, errors, and real-time updates." --- -Handle call session events to build responsive UIs. The SDK provides five event listener interfaces to monitor session status, participant activities, media changes, button clicks, and layout changes. Each listener is lifecycle-aware and automatically cleaned up when the Activity or Fragment is destroyed. +Handle call session events to build responsive UIs. The SDK provides five session listener interfaces to monitor session status, participant activities, media changes, button clicks, and layout changes, plus two global listeners for incoming calls and login state. Each listener is lifecycle-aware and automatically cleaned up when the Activity or Fragment is destroyed. ## Get CallSession Instance @@ -14,16 +14,18 @@ The `CallSession` is a singleton that manages the active call. All event listene ```kotlin -val callSession = CallSession.getInstance() +val callSession = CometChatCalls.getActiveSession() // null when no session is prepared or joined ``` ```java -CallSession callSession = CallSession.getInstance(); +CallSession callSession = CometChatCalls.getActiveSession(); // null when no session is prepared or joined ``` +`getActiveSession()` returns the session as soon as it is **prepared**, which is what `CometChatCalls.initiateCall` does on the caller and what the Push SDK's ring does on the callee, before anyone joins. That is what lets a caller listen for "ringing" and "answered" without joining (see [Ringing](/calls/android/ringing)). `CallSession.getInstance()` still works and returns the same object. + All event listeners are lifecycle-aware and automatically removed when the `LifecycleOwner` (Activity/Fragment) is destroyed. You don't need to manually remove listeners. @@ -60,7 +62,8 @@ callSession.addSessionStatusListener(this, object : SessionStatusListener() { } override fun onConnectionClosed() { - // Connection permanently closed + // Connection permanently closed. On a session that was only prepared + // (an outgoing call still ringing), this means the call ended before anyone joined. finish() } }) @@ -108,11 +111,11 @@ callSession.addSessionStatusListener(this, new SessionStatusListener() { | Event | Description | |-------|-------------| | `onSessionJoined()` | Successfully connected and joined the session | -| `onSessionLeft()` | Left the session via `leaveSession()` or was removed | -| `onSessionTimedOut()` | Session ended due to inactivity timeout | +| `onSessionLeft()` | Left the session via `leave()` or was removed | +| `onSessionTimedOut()` | Session ended due to inactivity timeout. On a prepared session: nobody joined in time | | `onConnectionLost()` | Network interrupted, SDK attempting reconnection | | `onConnectionRestored()` | Connection restored after being lost | -| `onConnectionClosed()` | Connection permanently closed, cannot reconnect | +| `onConnectionClosed()` | Connection permanently closed, cannot reconnect. On a prepared session: the call ended before anyone joined (declined, cancelled from another device, or the room went away); the reason arrives through `onIncomingCallEnded` | --- @@ -124,8 +127,13 @@ Monitor participant activities including join/leave, audio/video state, hand rai ```kotlin callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantConnected(participant: Participant) { + // A participant's device prepared the session but has not joined yet. + // On an outgoing call: the callee's phone is ringing. + } + override fun onParticipantJoined(participant: Participant) { - // A participant joined the call + // A participant joined the call. On an outgoing call: the callee answered. } override fun onParticipantLeft(participant: Participant) { @@ -146,16 +154,21 @@ callSession.addParticipantEventListener(this, object : ParticipantEventListener( override fun onParticipantStoppedScreenShare(participant: Participant) {} override fun onParticipantStartedRecording(participant: Participant) {} override fun onParticipantStoppedRecording(participant: Participant) {} - override fun onDominantSpeakerChanged(participant: Participant) {} }) ``` ```java callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantConnected(Participant participant) { + // A participant's device prepared the session but has not joined yet. + // On an outgoing call: the callee's phone is ringing. + } + @Override public void onParticipantJoined(Participant participant) { - // A participant joined the call + // A participant joined the call. On an outgoing call: the callee answered. } @Override @@ -176,7 +189,8 @@ callSession.addParticipantEventListener(this, new ParticipantEventListener() { | Event | Parameter | Description | |-------|-----------|-------------| -| `onParticipantJoined` | `Participant` | A participant connected to the call | +| `onParticipantConnected` | `Participant` | A participant's device prepared the session without joining. On the caller's prepared session this is the callee ringing | +| `onParticipantJoined` | `Participant` | A participant joined the call. On the caller's prepared session this is the callee answering: join now | | `onParticipantLeft` | `Participant` | A participant disconnected from the call | | `onParticipantListChanged` | `List` | Participant list updated | | `onParticipantAudioMuted` | `Participant` | A participant muted their microphone | @@ -189,7 +203,6 @@ callSession.addParticipantEventListener(this, new ParticipantEventListener() { | `onParticipantStoppedScreenShare` | `Participant` | A participant stopped screen sharing | | `onParticipantStartedRecording` | `Participant` | A participant started recording | | `onParticipantStoppedRecording` | `Participant` | A participant stopped recording | -| `onDominantSpeakerChanged` | `Participant` | The active speaker changed | --- @@ -474,3 +487,88 @@ callSession.addLayoutListener(this, new LayoutListener() { | `LayoutType.TILE` | Grid layout with equally-sized tiles | Group discussions, team meetings | | `LayoutType.SPOTLIGHT` | Large view for active speaker, small tiles for others | Presentations, one-on-one calls | + +--- + +## Incoming Call Events + +`CometChatCalls.addIncomingCallListener` is a global listener, not tied to a session. It reports a call for the logged-in user starting and ending, on both sides of the call, whether the call arrived by push while the app was killed or while it was running. It is how a caller learns why an outgoing call ended, and how your own screens stay in sync with calls the Push SDK rings. + + + +```kotlin +CometChatCalls.addIncomingCallListener("my-screen", object : CometChatCalls.IncomingCallListener() { + override fun onIncomingCallReceived(call: InitiatedCall) { + // A call for this user is ringing (the Push SDK already shows it) + } + + override fun onIncomingCallEnded(call: InitiatedCall, reason: String) { + // The ring ended, or your outgoing call ended: "rejected", "busy", "unanswered", "cancelled", "ongoing" + } +}) + +CometChatCalls.removeIncomingCallListener("my-screen") +``` + + +```java +CometChatCalls.addIncomingCallListener("my-screen", new CometChatCalls.IncomingCallListener() { + @Override + public void onIncomingCallReceived(InitiatedCall call) { + // A call for this user is ringing (the Push SDK already shows it) + } + + @Override + public void onIncomingCallEnded(InitiatedCall call, String reason) { + // The ring ended, or your outgoing call ended: "rejected", "busy", "unanswered", "cancelled", "ongoing" + } +}); + +CometChatCalls.removeIncomingCallListener("my-screen"); +``` + + + +| Event | Parameters | Description | +|-------|------------|-------------| +| `onIncomingCallReceived` | `InitiatedCall` | A call for the logged-in user is ringing | +| `onIncomingCallEnded` | `InitiatedCall`, `reason` | The call ended before being answered here. Caller: `rejected`, `busy`, `unanswered`, `cancelled`. Callee still ringing: `cancelled`, `ongoing` (answered elsewhere), `rejected` / `busy` (declined here), `unanswered` | + +`InitiatedCall` carries `sessionId`, `messageId`, `conversationId`, `sender`, `senderName`, `senderAvatar`, `receiver`, `receiverType`, `type`, `status` and `initiatedAt`. + + +Listener ids are keys: registering the same id twice replaces the earlier listener. This listener is not lifecycle-aware; remove it when the screen is destroyed. + + +--- + +## Login Events + +`CometChatCalls.addLoginListener` reports the Calls SDK login state. The Push SDK uses it to register and unregister the push token and to stop a ring when the user logs out; your app can use it to gate call UI. + + + +```kotlin +CometChatCalls.addLoginListener("my-app", object : CometChatCalls.LoginListener() { + override fun loginSuccess(user: CallUser) {} + override fun loginFailure(e: CometChatException) {} + override fun logoutSuccess() {} + override fun logoutFailure(e: CometChatException) {} +}) + +CometChatCalls.removeLoginListener("my-app") +``` + + +```java +CometChatCalls.addLoginListener("my-app", new CometChatCalls.LoginListener() { + @Override public void loginSuccess(CallUser user) {} + @Override public void loginFailure(CometChatException e) {} + @Override public void logoutSuccess() {} + @Override public void logoutFailure(CometChatException e) {} +}); + +CometChatCalls.removeLoginListener("my-app"); +``` + + diff --git a/calls/android/migrating-from-legacy-ringing.mdx b/calls/android/migrating-from-legacy-ringing.mdx new file mode 100644 index 000000000..7b1dc7ce2 --- /dev/null +++ b/calls/android/migrating-from-legacy-ringing.mdx @@ -0,0 +1,313 @@ +--- +title: "Migrating from Legacy Ringing" +sidebarTitle: "Migrating from Legacy Ringing" +sdk_version: "5.x" +description: "Move an Android app from Chat SDK call signalling and Push Notifications SDK 1.0 VoIP to ringing with the Calls SDK 5.1 and the Push Notifications SDK 2.0: what to add, what to delete, and how each method maps." +--- + +Legacy ringing signalled calls through the Chat SDK: `CometChat.initiateCall` created the call, a `CallListener` on the WebSocket told the callee, `acceptCall` and `rejectCall` answered, and the app rang the device itself, often with a hand-built `ConnectionService`. Ringing now runs on the Calls SDK and the Push Notifications SDK calls module. The Chat SDK's calling methods are deprecated. + +This page is the diff. For the full description of the current flow see [Ringing](/calls/android/ringing); for the push side see [Push ringing](/calls/android/voip-calling). + +## Why it changed + +| Legacy | Now | +|--------|-----| +| Incoming calls arrived on the Chat WebSocket, so a killed or backgrounded app needed a separate push + Telecom integration to ring | One path: the push rings in every app state, with the app killed included | +| Four pieces of app code rang the device (listener, overlay, ringtone, busy check) | The Push SDK rings, draws the incoming screen, handles busy, timeout and missed calls | +| `acceptCall` / `rejectCall` / `endCall` were HTTP calls your app had to sequence with `joinSession` | Answer = join. Decline, timeout and busy are sent by the Push SDK through a durable queue | +| Caller only knew "accepted" or "rejected" | Caller sees "Ringing…" (the callee's device was reached), "answered", and the reason a call ended (`rejected`, `busy`, `unanswered`, `cancelled`) | +| Chat SDK required for calling | Chat SDK needed for chat only; a calls-only app works | + +## Before and after + +```mermaid +sequenceDiagram + participant A as Caller + participant Chat as Chat SDK (WebSocket) + participant B as Callee + rect rgb(240,240,240) + Note over A,B: Legacy ringing + A->>Chat: CometChat.initiateCall(Call) + Chat-->>B: CallListener.onIncomingCallReceived + B->>B: App shows its own incoming UI, plays ringtone + B->>Chat: CometChat.acceptCall(sessionId) + Chat-->>A: CallListener.onOutgoingCallAccepted + A->>A: joinSession + B->>B: joinSession + A->>Chat: CometChat.endCall(sessionId) + Chat-->>B: onCallEndedMessageReceived + end +``` + +```mermaid +sequenceDiagram + participant A as Caller + participant BE as Backend + participant P as Callee · Push SDK + participant C as Callee · Calls SDK + rect rgb(232,244,236) + Note over A,C: Ringing now + A->>BE: CometChatCalls.initiateCall(uid, options) + BE-->>P: push (initiated) + P->>P: rings, shows incoming screen + P->>C: connect(sessionId) + C-->>A: onParticipantConnected ("Ringing…") + P->>C: Answer → answer activity → joinSession + C-->>A: onParticipantJoined → joinSession + A->>A: CallSession.leave() to hang up + end +``` + +## Step 1: Update dependencies + +```kotlin build.gradle.kts +dependencies { + implementation("com.cometchat:calls-sdk-android:5.1.0") // was 5.0.x + implementation("com.cometchat:push-notifications-android:2.0.0") // was 1.0.x + implementation("com.cometchat:push-notifications-calls-android:2.0.0") // new: the ringer + // chat-sdk-android stays for chat; it is no longer used for calls +} +``` + +- `minSdk` 26 or higher. +- Keep your Firebase Messaging dependency and `google-services.json`; the Push SDK never compiles against Firebase. + + +`setCallMode` defaults to `AUTO`, which is what this page assumes. Adding the `push-notifications-calls-android` artifact is the opt-in: core detects it and installs the ringer. There is no second initialiser. + + +## Step 2: Replace the push configuration + + + +```kotlin +CometChatPushNotifications.init(this, + PNConfiguration.Builder(appId, region) + .setNotificationSmallIcon(R.drawable.ic_notification) + .setVoIPEnabled(true) + .setInlineReplyEnabled(true) + .build()) + +CometChatPushNotifications.setOnCallAnsweredHandler(object : CallAnsweredHandler { + override fun onCallAnswered(context: Context, callInfo: PNCallInfo) { + CometChat.acceptCall(callInfo.sessionId, /* … then launch the call screen */) + } +}) +``` + + +```kotlin +CometChatPushNotifications.init(this, + PNConfiguration.Builder(appId, region) + .setProviderId(PROVIDER_ID) // your FCM provider from the dashboard + .setAnswerActivity(CallScreenActivity::class.java) // opened on Answer with the call in its intent + .setNotificationSmallIcon(R.drawable.ic_notification) + .setInlineReplyEnabled(true) + .build()) +``` + + + +If your Chat and Calls SDKs use `overrideClientHost` (a staging or dedicated deployment), pass the same host with `PNConfiguration.Builder.overrideClientHost(host)`. Otherwise the push token registers on the default host and no push ever arrives. + +## Step 3: Shrink the Firebase service + + + +```kotlin +override fun onMessageReceived(message: RemoteMessage) { + if (CometChatPushNotifications.isAppInForeground() && isCallPush(message.data)) { + return // the WebSocket CallListener handles it + } + CometChatPushNotifications.handlePushNotification(this, message.data) +} + +override fun onNewToken(token: String) { + CometChatPushNotifications.handleTokenRefresh(PushPlatform.FCM_ANDROID, token, PROVIDER_ID) +} +``` + + +```kotlin +override fun onMessageReceived(message: RemoteMessage) { + val handled = CometChatPushNotifications.handlePushNotification( + context = this, + data = message.data, + sentTimeMs = message.sentTime // lets the SDK drop a push delivered late + ) + if (!handled) { /* not a CometChat push */ } +} + +override fun onNewToken(token: String) { + CometChatPushNotifications.handleTokenRefresh(token) +} +``` + + + +Never skip call pushes in the foreground any more. The WebSocket no longer carries call signalling for your app; the push is the only signal, in every state. + +## Step 4: Delete the token code + +The Push SDK registers the token on login and unregisters it on logout by itself. Remove: + +- `FirebaseMessaging.getInstance().token` + `registerToken(...)` / `CometChatNotifications.registerPushToken(...)` after login +- `unregisterToken(...)` / `unregisterPushToken(...)` before logout + +Call `registerToken()` yourself only if you set `setAutoRegisterToken(false)`. + +## Step 5: Delete the app-side ringing + +Everything in this list is now done by the Push SDK. Remove it, or it will fight the SDK's ring (two ringtones, two screens, a busy reject sent on top of the SDK's). + +- The Application-level `CometChat.addCallListener(...)` and its `onIncomingCallReceived` handler +- The incoming-call overlay, snackbar, dialog or activity you launched from it +- Your own ringtone / sound manager for incoming calls +- The "already in a call → reject with busy" check +- `CometChatVoIP`, `PhoneAccount` registration, `CometChatVoIPConnectionService`, `CallConnection`, and the `READ_PHONE_STATE`, `MANAGE_OWN_CALLS` and `ANSWER_PHONE_CALLS` permission chain +- Any `isAppInForeground()` gate around call pushes + +Keep: `RECORD_AUDIO`, `CAMERA` and `POST_NOTIFICATIONS` in the manifest. The SDKs request them but do not declare them. + +## Step 6: Rewrite the call code + +| Legacy | Now | Notes | +|--------|-----|-------| +| `CometChat.initiateCall(Call(uid, RECEIVER_TYPE_USER, type), listener)` | `CometChatCalls.initiateCall(context, uid, InitiateCallOptions, listener)` | Set `options.sessionSettings` with the session type, or an audio call opens the camera | +| `CometChat.initiateCall(call, timeoutSeconds, listener)` | No client timeout. The ring window is 45 s on both sides (or the push's `ringTimeout`) | Run your own 45 s timer on the caller if you want the screen to close on its own | +| `CallListener.onIncomingCallReceived(call)` | Nothing to write; the Push SDK rings. For your own state use `IncomingCallListener.onIncomingCallReceived(InitiatedCall)` | One listener whether the call came by push on a killed process or on a live one | +| `CallListener.onOutgoingCallAccepted(call)` | `ParticipantEventListener.onParticipantJoined(participant)` on the session `initiateCall` prepared, via `CometChatCalls.getActiveSession()` | New in-between event: `onParticipantConnected` = the callee's device is ringing | +| `CallListener.onOutgoingCallRejected(call)` | `IncomingCallListener.onIncomingCallEnded(call, "rejected")` or `"busy"` | | +| `CallListener.onIncomingCallCancelled(call)` | `IncomingCallListener.onIncomingCallEnded(call, "cancelled")` on the callee; the Push SDK has already stopped the ring | | +| `CallListener.onCallEndedMessageReceived(call)` | `SessionStatusListener.onSessionLeft()` / `onConnectionClosed()` on the joined session; `onIncomingCallEnded(call, "unanswered")` while ringing | | +| `CometChat.acceptCall(sessionId, listener)` then `joinSession` | Your answer activity reads `CallBundles.getCall(intent)` and calls `joinSession`. Nothing else to send | The Push SDK asked for mic/camera before opening it | +| `CometChat.rejectCall(sessionId, CALL_STATUS_REJECTED, listener)` | Push SDK Decline button. Own incoming screen: `CometChatCallNotifications.decline(context, sessionId)` | Sends `busy` automatically when in another call | +| `CometChat.rejectCall(sessionId, CALL_STATUS_BUSY, listener)` | Nothing; the Push SDK decides busy | | +| `CometChat.rejectCall(sessionId, CALL_STATUS_CANCELLED, listener)` | `CometChatCalls.endCall(sessionId, Constants.CALL_STATUS_CANCELLED, listener)` | | +| `CallSession.leaveSession()` + `CometChat.endCall(sessionId, listener)` | `CallSession.leave()` | No status to send on a hang-up; the room tells the other side and the backend logs it | +| `CometChat.getActiveCall()` / `clearActiveCall()` | `CometChatCalls.getActiveSession()` (null when no session is prepared or joined) | | +| `CometChat.startCall(...)`, `CallSettings`, `OngoingCallListener` | `joinSession` + the 5.x listeners (see the [v5 migration guide](/calls/android/migration-guide-v5)) | | +| `CometChatCallsConstants.*` | `com.cometchat.calls.constants.Constants.*` | The whole `CometChatCallsConstants` class is deprecated in 5.1 | +| `CometChat.logout(...)` | `CometChatCalls.logout(...)` as well (the UI Kit's `CometChatUIKit.logout` does both) | Otherwise the Calls SDK stays signed in as the old user and a ring is not stopped | + +### Caller screen, before and after + + + +```kotlin +CometChat.addCallListener(LISTENER_ID, object : CometChat.CallListener() { + override fun onOutgoingCallAccepted(call: Call) = joinCallSession(call.sessionId) + override fun onOutgoingCallRejected(call: Call) = finish() + override fun onIncomingCallReceived(call: Call) {} + override fun onIncomingCallCancelled(call: Call) {} +}) + +endButton.setOnClickListener { + CometChat.rejectCall(sessionId, CometChatConstants.CALL_STATUS_CANCELLED, callback) +} +``` + + +```kotlin +val session = CometChatCalls.getActiveSession() +session?.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantConnected(p: Participant) { if (p.uid != me) status.text = "Ringing…" } + override fun onParticipantJoined(p: Participant) { if (p.uid != me) joinCallSession(sessionId, type) } +}) +CometChatCalls.addIncomingCallListener(LISTENER_ID, object : CometChatCalls.IncomingCallListener() { + override fun onIncomingCallReceived(call: InitiatedCall) {} + override fun onIncomingCallEnded(call: InitiatedCall, reason: String) { + if (call.sessionId == sessionId) finishWith(reason) // rejected, busy, unanswered, cancelled + } +}) + +endButton.setOnClickListener { + CometChatCalls.endCall(sessionId, Constants.CALL_STATUS_CANCELLED, callback) +} +``` + + + +### Callee screen, before and after + + + +```kotlin +// Launched by your CallListener / ConnectionService +acceptButton.setOnClickListener { + CometChat.acceptCall(sessionId, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) = joinCallSession(call.sessionId) + override fun onError(e: CometChatException) = finish() + }) +} +declineButton.setOnClickListener { + CometChat.rejectCall(sessionId, CometChatConstants.CALL_STATUS_REJECTED, callback) +} +``` + + +```kotlin +// Launched by the Push SDK on Answer (PNConfiguration.setAnswerActivity) +override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val call = CallBundles.getCall(intent) ?: return finish() + joinCallSession(call.sessionId, call.callType) // the user already answered +} +// There is no decline button here: the Push SDK's ring screen and notification handle it. +``` + + + +## Step 7: Change the logout + + + +```kotlin +CometChat.logout(chatCallback) +CometChatCalls.logout(callsCallback) // new: stops a ringing call, unregisters the push token +``` + + +```kotlin +CometChatUIKit.logout(callback) // instead of CometChat.logout: logs out of Chat and Calls +``` + + + +## If you drew your own incoming-call screen + +Keep it, but drive it from the Push SDK instead of the `CallListener`: + +1. Register it with `PNConfiguration.Builder.setIncomingCallActivity(YourRingActivity::class.java)`. The Push SDK still rings, pre-warms the session, times out and sends the decline; only the screen is yours. +2. In `onCreate`, read `CallBundles.getCall(intent)` and call `CometChatCallNotifications.onIncomingCallScreenOpened(sessionId) { reason -> finish() }` so the SDK closes the screen when the call ends elsewhere. +3. Answer with `CometChatCallNotifications.answer(context, sessionId)` after asking for the microphone (and camera) yourself; `answer()` skips the SDK's own prompt. +4. Decline with `CometChatCallNotifications.decline(context, sessionId)`. +5. In `onDestroy`, call `CometChatCallNotifications.onIncomingCallScreenClosed(sessionId)`. + +Details on the [Push ringing](/calls/android/voip-calling) page. + +## What users will notice + +- Calls ring with the app killed, from the lock screen, without Telecom permissions. +- The caller sees "Ringing…" once the callee's phone is reached, and the reason when a call is declined or missed. +- Both sides stop ringing after 45 seconds. +- Missed calls appear as notifications on the callee. +- A call during a call is call waiting (answer ends the current call, decline sends busy), not an automatic reject. +- The microphone and camera prompt appears on Answer the first time, on the device that answers. + +## Checklist + +- [ ] Dependencies bumped, `push-notifications-calls-android` added, `minSdk 26` +- [ ] `PNConfiguration` has `setProviderId` and `setAnswerActivity`; `overrideClientHost` matches the Chat and Calls SDKs if you use one +- [ ] Firebase service passes every push to `handlePushNotification` with `message.sentTime` +- [ ] Manual token registration removed +- [ ] `CometChat.addCallListener`, own incoming UI, ringtone, busy check and the ConnectionService stack removed +- [ ] `initiateCall` sets the session type in `sessionSettings` +- [ ] Caller listens on the prepared session and on `IncomingCallListener`; joins only on `onParticipantJoined` +- [ ] Answer activity reads `CallBundles.getCall(intent)` and joins +- [ ] Hang-up uses `leave()`; no `CometChat.endCall` +- [ ] Logout also logs out of the Calls SDK +- [ ] `ensureRingReady` runs after sign-in and anything other than `READY` is shown to the user +- [ ] Call pushes are sent at high FCM priority +- [ ] Tested: ring with the app killed, decline, cancel, no answer, call during a call, logout while ringing diff --git a/calls/android/migration-guide-v5.mdx b/calls/android/migration-guide-v5.mdx index d504536c5..45f6d3900 100644 --- a/calls/android/migration-guide-v5.mdx +++ b/calls/android/migration-guide-v5.mdx @@ -266,7 +266,7 @@ CometChatCalls.stopRecording(); ```java CallSession callSession = CallSession.getInstance(); -callSession.leaveSession(); +callSession.leave(); callSession.switchCamera(); callSession.muteAudio(); // or callSession.unmuteAudio() callSession.pauseVideo(); // or callSession.resumeVideo() @@ -283,7 +283,7 @@ callSession.stopRecording(); | v4 Static Method | v5 CallSession Method | |---|---| -| `CometChatCalls.endSession()` | `callSession.leaveSession()` | +| `CometChatCalls.endSession()` | `callSession.leave()` | | `CometChatCalls.switchCamera()` | `callSession.switchCamera()` | | `CometChatCalls.muteAudio(true)` | `callSession.muteAudio()` | | `CometChatCalls.muteAudio(false)` | `callSession.unmuteAudio()` | @@ -464,3 +464,19 @@ These classes still exist in v5 for backward compatibility but are deprecated: | `CometChatCallsEventsListener` | 5 focused listeners on `CallSession` | | `RTCRecordingInfo` | `MediaEventsListener` callbacks | | `CallSwitchRequestInfo` | *Removed (no replacement)* | + +--- + +## 5.0 to 5.1 + +5.1 moves ringing onto the Calls SDK and the Push Notifications SDK, and deprecates two more things: + +| Deprecated in 5.1 | Replacement | +|-------------------|-------------| +| `CallSession.leaveSession()` | `CallSession.leave()` | +| `CometChatCallsConstants` (the whole class) | `com.cometchat.calls.constants.Constants` | +| Chat SDK calling: `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall`, `addCallListener`, `CallListener`, `CallSettings` | `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `addIncomingCallListener`, the Push Notifications SDK calls module, `leave()` | + +New in 5.1: `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `connect`, `getActiveSession`, `addIncomingCallListener` / `IncomingCallListener`, `addLoginListener` / `LoginListener`, `ParticipantEventListener.onParticipantConnected`, `CallAppSettingBuilder.overrideAdminHost / overrideClientHost / overrideCallsHost`. + +The step-by-step move from Chat SDK signalling is on [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). diff --git a/calls/android/overview.mdx b/calls/android/overview.mdx index b5aff0b4c..ad2643e05 100644 --- a/calls/android/overview.mdx +++ b/calls/android/overview.mdx @@ -28,20 +28,22 @@ sequenceDiagram App->>CometChatCalls: init() App->>CometChatCalls: login() - App->>CometChatCalls: generateToken() + App->>CometChatCalls: initiateCall() — caller, or answer from the push ring — callee App->>CometChatCalls: joinSession() CometChatCalls-->>App: CallSession App->>CallSession: Actions (mute, pause, etc.) CallSession-->>App: Event callbacks - App->>CallSession: leaveSession() + App->>CallSession: leave() ``` +Ringing the other user is done by the Push Notifications SDK's calls module, not by the Chat SDK: see [Ringing](/calls/android/ringing) and [Push Ringing](/calls/android/voip-calling). A session can also be joined directly by id for a group call or a meeting. + ## Features - Incoming and outgoing call notifications with accept/reject functionality + Start a call, ring the callee through push, show "Ringing…", answer, decline, cancel and end diff --git a/calls/android/ringing.mdx b/calls/android/ringing.mdx index 058f9ff70..1dfa4276a 100644 --- a/calls/android/ringing.mdx +++ b/calls/android/ringing.mdx @@ -2,379 +2,423 @@ title: "Ringing" sidebarTitle: "Ringing" sdk_version: "5.x" -description: "Configure CometChat Calls SDK v5 ringing on Android for incoming calls, outgoing calls, call alerts, and accept or reject flows." +description: "Ring users on Android with the CometChat Calls SDK and the Push Notifications SDK: start a call, show it ringing, answer, decline, cancel and end it, with the app in any state." --- -Implement incoming and outgoing call notifications with accept/reject functionality. Ringing enables real-time call signaling between users, allowing them to initiate calls and respond to incoming call requests. +Ringing lets one user call another and lets the callee answer or decline, whether the callee's app is in the foreground, in the background or not running at all. + +Two SDKs do the work: + +| SDK | Owns | +|-----|------| +| **Calls SDK** (`calls-sdk-android` 5.1+) | Creating the call, the media session, answering, declining, cancelling, ending, and the call events your code listens to | +| **Push Notifications SDK, calls module** (`push-notifications-calls-android` 2.0+) | Ringing the callee's device: the call notification, the full-screen incoming-call screen, the ringtone, the ring timeout, missed calls and busy handling | + +The Chat SDK is not part of ringing. It is still needed for chat, but nothing on this page calls it. -Ringing functionality requires the CometChat Chat SDK to be integrated alongside the Calls SDK. The Chat SDK handles call signaling (initiating, accepting, rejecting calls), while the Calls SDK manages the actual call session. +**Coming from the earlier ringing flow?** The Chat SDK's calling methods (`CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `CallListener`) are deprecated and are replaced by what this page describes. See [Migrating from legacy ringing](/calls/android/migrating-from-legacy-ringing) for a method-by-method mapping. -## How Ringing Works - -The ringing flow involves two SDKs working together: +## How ringing works -1. **Chat SDK** - Handles call signaling (initiate, accept, reject, cancel) -2. **Calls SDK** - Manages the actual call session once accepted +The caller creates the call with the Calls SDK. The CometChat backend sends a push to the callee. The Push SDK rings the callee's device, and when the callee answers, your call screen joins the session. The caller's app learns that the callee is ringing and has answered from the Calls SDK, without joining the session first. ```mermaid sequenceDiagram - participant Caller - participant ChatSDK - participant Receiver - participant CallsSDK - - Caller->>ChatSDK: initiateCall() - ChatSDK->>Receiver: onIncomingCallReceived - Receiver->>ChatSDK: acceptCall() - ChatSDK-->>Caller: onOutgoingCallAccepted - Caller->>CallsSDK: joinSession() - Receiver->>CallsSDK: joinSession() + participant A as Caller app + participant BE as CometChat backend + participant P as Callee · Push SDK + participant C as Callee · Calls SDK + + A->>BE: CometChatCalls.initiateCall(receiverId, options) + Note over A: Session prepared, not joined ("Calling…") + BE-->>P: Push (callAction = initiated) + P->>P: Ring: notification, full-screen screen, ringtone + P->>C: connect(sessionId) — pre-warms the session + C-->>A: onParticipantConnected → "Ringing…" + P->>P: Answer → microphone/camera prompt → your answer activity + C->>C: joinSession(sessionId) + C-->>A: onParticipantJoined → caller joins too + Note over A,C: In call. Hang up with CallSession.leave() +``` + +What each side sees: + +| Stage | Caller | Callee | +|-------|--------|--------| +| Call created | `initiateCall` succeeds; show "Calling…" | Push arrives; the Push SDK rings | +| Callee's device reached | `onParticipantConnected` → show "Ringing…" | Ringing | +| Callee answers | `onParticipantJoined` → `joinSession` | Answer activity opens → `joinSession` | +| Callee declines / is busy / does not answer | `onIncomingCallEnded(call, "rejected" / "busy" / "unanswered")` | Ring stops; missed call if unanswered | +| Caller cancels | — (you ended it) | Ring stops; missed call | +| Either side hangs up | `onParticipantLeft` / `onSessionLeft` | same | + +## Prerequisites + +- [Calls SDK set up](/calls/android/setup) and the user [logged in](/calls/android/authentication). +- Push notifications configured with the Push Notifications SDK 2.0 calls module. The whole push side of ringing is on the [Push ringing](/calls/android/voip-calling) page; this page assumes it is done. +- `RECORD_AUDIO` and `CAMERA` declared in your manifest. The SDKs request them at runtime but do not declare them. +- `minSdk 26`. + +```kotlin build.gradle.kts +dependencies { + implementation("com.cometchat:calls-sdk-android:5.1.0") + implementation("com.cometchat:push-notifications-android:2.0.0") + implementation("com.cometchat:push-notifications-calls-android:2.0.0") +} ``` -## Initiate a Call + +Send call pushes at **high** FCM priority. At normal priority the call still rings, but Android refuses the Push SDK's foreground service, so the process can be killed between the ring and the answer. + -Use the Chat SDK to initiate a call to a user or group: +## Start a call + +`CometChatCalls.initiateCall` creates the call on the backend, which rings the callee, and prepares the caller's media session. The caller does **not** join yet. ```kotlin -val receiverID = "USER_ID" -val receiverType = CometChatConstants.RECEIVER_TYPE_USER -val callType = CometChatConstants.CALL_TYPE_VIDEO +val options = InitiateCallOptions().apply { + type = Constants.SESSION_TYPE_AUDIO // or SESSION_TYPE_VIDEO + receiverType = Constants.RECEIVER_TYPE_USER + sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setSessionType(SessionType.VOICE) // VIDEO for a video call + .build() +} -val call = Call(receiverID, receiverType, callType) +CometChatCalls.initiateCall(context, receiverUid, options, + object : CometChatCalls.CallbackListener() { + override fun onSuccess(call: InitiatedCall) { + Log.d(TAG, "Calling ${call.receiver}, session ${call.sessionId}") + showOutgoingScreen(call) + listenOnPreparedSession(call) + } -CometChat.initiateCall(call, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call initiated: ${call.sessionId}") - // Show outgoing call UI + override fun onError(e: CometChatException) { + Log.e(TAG, "Could not start the call: ${e.message}") + } } +) +``` + + +```java +InitiateCallOptions options = new InitiateCallOptions(); +options.setType(Constants.SESSION_TYPE_AUDIO); // or SESSION_TYPE_VIDEO +options.setReceiverType(Constants.RECEIVER_TYPE_USER); +options.setSessionSettings(new CometChatCalls.SessionSettingsBuilder() + .setSessionType(SessionType.VOICE) // VIDEO for a video call + .build()); + +CometChatCalls.initiateCall(context, receiverUid, options, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(InitiatedCall call) { + Log.d(TAG, "Calling " + call.getReceiver() + ", session " + call.getSessionId()); + showOutgoingScreen(call); + listenOnPreparedSession(call); + } - override fun onError(e: CometChatException) { - Log.e(TAG, "Call initiation failed: ${e.message}") + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Could not start the call: " + e.getMessage()); + } } -}) +); ``` - -```kotlin -val receiverID = "USER_ID" -val receiverType = CometChatConstants.RECEIVER_TYPE_USER -val callType = CometChatConstants.CALL_TYPE_VIDEO + -val call = Call(receiverID, receiverType, callType) +| Parameter | Description | +|-----------|-------------| +| `receiverUid` | UID of the user to call | +| `options.type` | `Constants.SESSION_TYPE_AUDIO` or `Constants.SESSION_TYPE_VIDEO` | +| `options.receiverType` | `Constants.RECEIVER_TYPE_USER` | +| `options.sessionSettings` | The settings the caller's session is prepared with. See the warning below. | -// Set a custom timeout of 30 seconds (default is 45 seconds) -CometChat.initiateCall(call, 30, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call initiated: ${call.sessionId}") - // Show outgoing call UI - } +`InitiatedCall` gives you `sessionId`, `messageId` (the id of the call message in the conversation), `sender`, `senderName`, `senderAvatar`, `receiver`, `receiverType`, `type`, `status`, `initiatedAt` and `conversationId`. - override fun onError(e: CometChatException) { - Log.e(TAG, "Call initiation failed: ${e.message}") - } -}) -``` - - -```java -String receiverID = "USER_ID"; -String receiverType = CometChatConstants.RECEIVER_TYPE_USER; -String callType = CometChatConstants.CALL_TYPE_VIDEO; + +**Always set the session type in `sessionSettings`.** The Calls SDK freezes the session settings when it prepares the session and its default is video, so an audio call started without `setSessionType(SessionType.VOICE)` opens the caller's camera. The settings you pass here are the ones the caller later joins with, so pass the same builder to `joinSession`. + -Call call = new Call(receiverID, receiverType, callType); + +There is no client-side ring timeout. Both sides stop at the ring window the backend sends in the push (45 seconds by default). A caller that wants its own screen to time out can end the call as `unanswered` after the same window, as shown under [Ring timeout](#ring-timeout). + -CometChat.initiateCall(call, new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call initiated: " + call.getSessionId()); - // Show outgoing call UI - } +## Show "Ringing…" and detect the answer - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Call initiation failed: " + e.getMessage()); - } -}); +The caller stays on the session that `initiateCall` prepared and listens to it. Do not call `joinSession` yet: joining only when the callee has joined keeps the caller's media off until there is someone to talk to, and it is what makes the two events below arrive. + + + +```kotlin +fun listenOnPreparedSession(call: InitiatedCall) { + val session = CometChatCalls.getActiveSession() ?: return + val me = CometChatCalls.getLoggedInUser()?.uid + + session.addParticipantEventListener(this /* LifecycleOwner */, object : ParticipantEventListener() { + // The callee's device received the push and is ringing. + override fun onParticipantConnected(participant: Participant) { + if (participant.uid == me) return + setStatus("Ringing…") + } + + // The callee answered: now the caller joins. + override fun onParticipantJoined(participant: Participant) { + if (participant.uid == me) return + joinCallSession(call.sessionId, call.type) + } + }) + + session.addSessionStatusListener(this, object : SessionStatusListener() { + // The prepared session closed before anyone joined: the call is over. + // The reason (rejected, busy, …) arrives through onIncomingCallEnded, usually first. + override fun onConnectionClosed() = closeOutgoingScreen() + override fun onSessionTimedOut() = closeOutgoingScreen() + }) +} ``` - + ```java -String receiverID = "USER_ID"; -String receiverType = CometChatConstants.RECEIVER_TYPE_USER; -String callType = CometChatConstants.CALL_TYPE_VIDEO; - -Call call = new Call(receiverID, receiverType, callType); +void listenOnPreparedSession(InitiatedCall call) { + CallSession session = CometChatCalls.getActiveSession(); + if (session == null) return; + CallUser loggedIn = CometChatCalls.getLoggedInUser(); + String me = loggedIn != null ? loggedIn.getUid() : null; + + session.addParticipantEventListener(this /* LifecycleOwner */, new ParticipantEventListener() { + // The callee's device received the push and is ringing. + @Override + public void onParticipantConnected(Participant participant) { + if (participant.getUid().equals(me)) return; + setStatus("Ringing…"); + } -// Set a custom timeout of 30 seconds (default is 45 seconds) -CometChat.initiateCall(call, 30, new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call initiated: " + call.getSessionId()); - // Show outgoing call UI - } + // The callee answered: now the caller joins. + @Override + public void onParticipantJoined(Participant participant) { + if (participant.getUid().equals(me)) return; + joinCallSession(call.getSessionId(), call.getType()); + } + }); - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Call initiation failed: " + e.getMessage()); - } -}); + session.addSessionStatusListener(this, new SessionStatusListener() { + // The prepared session closed before anyone joined: the call is over. + // The reason (rejected, busy, …) arrives through onIncomingCallEnded, usually first. + @Override public void onConnectionClosed() { closeOutgoingScreen(); } + @Override public void onSessionTimedOut() { closeOutgoingScreen(); } + }); +} ``` -| Parameter | Type | Description | -|-----------|------|-------------| -| `receiverID` | String | UID of the user or GUID of the group to call | -| `receiverType` | String | `CometChatConstants.RECEIVER_TYPE_USER` or `RECEIVER_TYPE_GROUP` | -| `callType` | String | `CometChatConstants.CALL_TYPE_VIDEO` or `CALL_TYPE_AUDIO` | -| `timeout` | int | Optional. The timeout duration in seconds for the call to be answered before it's automatically cancelled. Defaults to 45 seconds. | +| Event | On the prepared session it means | +|-------|----------------------------------| +| `onParticipantConnected(callee)` | The callee's phone got the push and the Push SDK pre-warmed the session: it is ringing there | +| `onParticipantJoined(callee)` | The callee answered. Call `joinSession` now | +| `onConnectionClosed` / `onSessionTimedOut` | The call ended before anyone joined (declined, cancelled from another device, or the session died) | + +Both listeners are lifecycle-aware: pass your activity or fragment as the `LifecycleOwner` and the SDK removes them when it is destroyed. -## Listen for Incoming Calls +## Listen for calls starting and ending -Register a call listener to receive incoming call notifications: +`CometChatCalls.addIncomingCallListener` is the one place a call announces itself and its end, on both sides. The Push SDK feeds it from the push, so it fires even when nothing of yours was running when the push arrived. ```kotlin -val listenerID = "UNIQUE_LISTENER_ID" - -CometChat.addCallListener(listenerID, object : CometChat.CallListener() { - override fun onIncomingCallReceived(call: Call) { - Log.d(TAG, "Incoming call from: ${call.callInitiator.name}") - // Show incoming call UI with accept/reject options +CometChatCalls.addIncomingCallListener("my-screen", object : CometChatCalls.IncomingCallListener() { + override fun onIncomingCallReceived(call: InitiatedCall) { + // A call for this user is ringing. The Push SDK already shows the ring; + // use this for your own state (badge, analytics, refreshing a list). } - override fun onOutgoingCallAccepted(call: Call) { - Log.d(TAG, "Call accepted, joining session...") - joinCallSession(call.sessionId) - } - - override fun onOutgoingCallRejected(call: Call) { - Log.d(TAG, "Call rejected") - // Dismiss outgoing call UI - } - - override fun onIncomingCallCancelled(call: Call) { - Log.d(TAG, "Incoming call cancelled") - // Dismiss incoming call UI - } - - override fun onCallEndedMessageReceived(call: Call) { - Log.d(TAG, "Call ended") + override fun onIncomingCallEnded(call: InitiatedCall, reason: String) { + // Callee: the ring ended (caller cancelled, answered elsewhere, timed out, declined here). + // Caller: your outgoing call ended with reason "rejected", "busy", "cancelled" or "unanswered". + if (call.sessionId == currentSessionId) closeOutgoingScreen(reason) } }) + +// When the screen goes away: +CometChatCalls.removeIncomingCallListener("my-screen") ``` ```java -String listenerID = "UNIQUE_LISTENER_ID"; - -CometChat.addCallListener(listenerID, new CometChat.CallListener() { - @Override - public void onIncomingCallReceived(Call call) { - Log.d(TAG, "Incoming call from: " + call.getCallInitiator().getName()); - // Show incoming call UI with accept/reject options - } - - @Override - public void onOutgoingCallAccepted(Call call) { - Log.d(TAG, "Call accepted, joining session..."); - joinCallSession(call.getSessionId()); - } - +CometChatCalls.addIncomingCallListener("my-screen", new CometChatCalls.IncomingCallListener() { @Override - public void onOutgoingCallRejected(Call call) { - Log.d(TAG, "Call rejected"); - // Dismiss outgoing call UI + public void onIncomingCallReceived(InitiatedCall call) { + // A call for this user is ringing. The Push SDK already shows the ring; + // use this for your own state (badge, analytics, refreshing a list). } @Override - public void onIncomingCallCancelled(Call call) { - Log.d(TAG, "Incoming call cancelled"); - // Dismiss incoming call UI - } - - @Override - public void onCallEndedMessageReceived(Call call) { - Log.d(TAG, "Call ended"); + public void onIncomingCallEnded(InitiatedCall call, String reason) { + // Callee: the ring ended (caller cancelled, answered elsewhere, timed out, declined here). + // Caller: your outgoing call ended with reason "rejected", "busy", "cancelled" or "unanswered". + if (call.getSessionId().equals(currentSessionId)) closeOutgoingScreen(reason); } }); + +// When the screen goes away: +CometChatCalls.removeIncomingCallListener("my-screen"); ``` -| Callback | Description | -|----------|-------------| -| `onIncomingCallReceived` | A new incoming call is received | -| `onOutgoingCallAccepted` | The receiver accepted your outgoing call | -| `onOutgoingCallRejected` | The receiver rejected your outgoing call | -| `onIncomingCallCancelled` | The caller cancelled the incoming call | -| `onCallEndedMessageReceived` | The call has ended | +| Who | `onIncomingCallEnded` reason | +|-----|------------------------------| +| Caller | `rejected` (callee declined), `busy` (callee was in another call), `unanswered` (nobody answered), `cancelled` (you cancelled from another device). Never `ongoing` or `ended`. | +| Callee, still ringing | `cancelled` (caller gave up), `ongoing` (answered on another device), `rejected` / `busy` (declined here), `unanswered` (ring timed out) | +| Callee, answered | Nothing; the joined session reports the end through `onSessionLeft` / `onConnectionClosed` | -Remember to remove the call listener when it's no longer needed to prevent memory leaks: -```kotlin -CometChat.removeCallListener(listenerID) -``` +Listener ids are keys. Registering the same id twice replaces the first listener instead of adding a second one, so give each screen a stable id and remove it when the screen is destroyed. -## Accept a Call +## Answer a call -When an incoming call is received, accept it using the Chat SDK: +The Push SDK rings and, on **Answer**, asks for the microphone (and the camera for a video call), then starts the activity you registered with `PNConfiguration.setAnswerActivity`. That activity reads the call from its intent and joins. There is no separate "accept" call to make. ```kotlin -fun acceptIncomingCall(sessionId: String) { - CometChat.acceptCall(sessionId, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call accepted") - joinCallSession(call.sessionId) - } - - override fun onError(e: CometChatException) { - Log.e(TAG, "Accept call failed: ${e.message}") - } - }) +class CallScreenActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + setShowWhenLocked(true) + setTurnScreenOn(true) + + val call = CallBundles.getCall(intent) ?: return finish() + joinCallSession(call.sessionId, call.callType) + } } ``` ```java -void acceptIncomingCall(String sessionId) { - CometChat.acceptCall(sessionId, new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call accepted"); - joinCallSession(call.getSessionId()); - } - - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Accept call failed: " + e.getMessage()); - } - }); +public class CallScreenActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + setShowWhenLocked(true); + setTurnScreenOn(true); + + IncomingCall call = CallBundles.getCall(getIntent()); + if (call == null) { finish(); return; } + joinCallSession(call.getSessionId(), call.getCallType()); + } } ``` -## Reject a Call +`IncomingCall` carries `sessionId`, `callerUid`, `callerName`, `callerAvatarUrl`, `callType`, `receiverType`, `conversationId` and `isVideo`. -Reject an incoming call: +## Join the session + +Both sides join the same way: the callee from the answer activity, the caller from `onParticipantJoined`. `joinSession` takes over the session that was prepared earlier, so it connects almost at once. ```kotlin -fun rejectIncomingCall(sessionId: String) { - val status = CometChatConstants.CALL_STATUS_REJECTED +fun joinCallSession(sessionId: String, callType: String) { + val container = findViewById(R.id.call_view_container) + val isVideo = callType.equals(Constants.SESSION_TYPE_VIDEO, ignoreCase = true) - CometChat.rejectCall(sessionId, status, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call rejected") - // Dismiss incoming call UI - } + val settings = CometChatCalls.SessionSettingsBuilder() + .setSessionType(if (isVideo) SessionType.VIDEO else SessionType.VOICE) + .build() - override fun onError(e: CometChatException) { - Log.e(TAG, "Reject call failed: ${e.message}") + CometChatCalls.joinSession(sessionId, settings, container, + object : CometChatCalls.CallbackListener() { + override fun onSuccess(session: CallSession) { + Log.d(TAG, "Joined $sessionId") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Join failed: ${e.message}") + finish() + } } - }) + ) } ``` ```java -void rejectIncomingCall(String sessionId) { - String status = CometChatConstants.CALL_STATUS_REJECTED; +void joinCallSession(String sessionId, String callType) { + RelativeLayout container = findViewById(R.id.call_view_container); + boolean isVideo = Constants.SESSION_TYPE_VIDEO.equalsIgnoreCase(callType); - CometChat.rejectCall(sessionId, status, new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call rejected"); - // Dismiss incoming call UI - } + SessionSettings settings = new CometChatCalls.SessionSettingsBuilder() + .setSessionType(isVideo ? SessionType.VIDEO : SessionType.VOICE) + .build(); - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Reject call failed: " + e.getMessage()); + CometChatCalls.joinSession(sessionId, settings, container, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession session) { + Log.d(TAG, "Joined " + sessionId); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Join failed: " + e.getMessage()); + finish(); + } } - }); + ); } ``` -## Cancel a Call +See [Join Session](/calls/android/join-session) for the container, [Session Settings](/calls/android/session-settings) for layouts and controls, and [Events](/calls/android/events) for everything the joined session reports. -Cancel an outgoing call before it's answered: +## Decline a call + +Declining is handled by the Push SDK: the Decline button on the notification and on its incoming-call screen sends `endCall(sessionId, "rejected")` through a queue that survives the process dying, and sends `"busy"` instead when the user is already in another call. The caller receives `onIncomingCallEnded` with that reason. + +You only write decline code when you draw your own incoming-call screen: ```kotlin -fun cancelOutgoingCall(sessionId: String) { - val status = CometChatConstants.CALL_STATUS_CANCELLED - - CometChat.rejectCall(sessionId, status, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call cancelled") - // Dismiss outgoing call UI - } - - override fun onError(e: CometChatException) { - Log.e(TAG, "Cancel call failed: ${e.message}") - } - }) -} +CometChatCallNotifications.decline(context, sessionId) ``` ```java -void cancelOutgoingCall(String sessionId) { - String status = CometChatConstants.CALL_STATUS_CANCELLED; - - CometChat.rejectCall(sessionId, status, new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call cancelled"); - // Dismiss outgoing call UI - } - - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Cancel call failed: " + e.getMessage()); - } - }); -} +CometChatCallNotifications.decline(context, sessionId); ``` -## Join the Call Session +Without the Push SDK calls module, decline through the Calls SDK directly: `CometChatCalls.endCall(sessionId, Constants.CALL_STATUS_REJECTED, listener)`. + +## Cancel a call -After accepting a call (or when your outgoing call is accepted), join the call session using the Calls SDK: +The caller ends a call that is still ringing with `endCall` and the `cancelled` status. The callee's ring stops and a missed call is posted there. ```kotlin -fun joinCallSession(sessionId: String) { - val callViewContainer = findViewById(R.id.call_view_container) - - val sessionSettings = CometChatCalls.SessionSettingsBuilder() - .setSessionType(SessionType.VIDEO) - .build() - - CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, - object : CometChatCalls.CallbackListener() { - override fun onSuccess(callSession: CallSession) { - Log.d(TAG, "Joined call session") - } - +fun cancelOutgoingCall(sessionId: String) { + CometChatCalls.endCall(sessionId, Constants.CALL_STATUS_CANCELLED, + object : CometChatCalls.CallbackListener() { + override fun onSuccess(result: Void?) = closeOutgoingScreen() override fun onError(e: CometChatException) { - Log.e(TAG, "Failed to join: ${e.message}") + Log.e(TAG, "Cancel failed: ${e.message}") + closeOutgoingScreen() // close locally either way } } ) @@ -383,23 +427,13 @@ fun joinCallSession(sessionId: String) { ```java -void joinCallSession(String sessionId) { - RelativeLayout callViewContainer = findViewById(R.id.call_view_container); - - SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() - .setSessionType(SessionType.VIDEO) - .build(); - - CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, - new CometChatCalls.CallbackListener() { - @Override - public void onSuccess(CallSession callSession) { - Log.d(TAG, "Joined call session"); - } - - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Failed to join: " + e.getMessage()); +void cancelOutgoingCall(String sessionId) { + CometChatCalls.endCall(sessionId, Constants.CALL_STATUS_CANCELLED, + new CometChatCalls.CallbackListener() { + @Override public void onSuccess(Void result) { closeOutgoingScreen(); } + @Override public void onError(CometChatException e) { + Log.e(TAG, "Cancel failed: " + e.getMessage()); + closeOutgoingScreen(); // close locally either way } } ); @@ -408,138 +442,131 @@ void joinCallSession(String sessionId) { - -For more details on session customization and controlling the call, see [Actions](/calls/android/actions) and [Listeners](/calls/android/events). - +Disable the End button after the first tap, and ignore an `onParticipantJoined` that arrives after you cancelled: the cancel wins. -## End a Call +## Ring timeout -Properly ending a call requires coordination between both SDKs to ensure all participants are notified and call logs are recorded correctly. +The callee's Push SDK ends an unanswered 1:1 call as `unanswered` when its ring window closes (45 seconds unless the push carries a `ringTimeout`). To close the caller's screen at the same moment even if that signal is late, run your own timer from `initiateCall` and end the call with the same status: - -Always call `CometChat.endCall()` when ending a call. This notifies the other participant and ensures the call is properly logged. Without this, the other user won't know the call has ended and call logs may be incomplete. - - -```mermaid -sequenceDiagram - participant User - participant CallsSDK - participant ChatSDK - participant OtherParticipant - - User->>CallsSDK: leaveSession() - User->>ChatSDK: endCall(sessionId) - ChatSDK->>OtherParticipant: onCallEndedMessageReceived - OtherParticipant->>CallsSDK: leaveSession() + + +```kotlin +handler.postDelayed({ + if (!answered) { + CometChatCalls.endCall(sessionId, Constants.CALL_STATUS_UNANSWERED, callback) + closeOutgoingScreen() + } +}, 45_000L) +``` + + +```java +handler.postDelayed(() -> { + if (!answered) { + CometChatCalls.endCall(sessionId, Constants.CALL_STATUS_UNANSWERED, callback); + closeOutgoingScreen(); + } +}, 45_000L); ``` + + -When using the default call UI, listen for the end call button click using `ButtonClickListener` and call `endCall()`: +## End a call + +Hang up by leaving the session. The room tells the other side, which receives `onParticipantLeft` and then `onSessionLeft` or `onConnectionClosed`. No status needs to be sent and the call is logged by the backend. ```kotlin -val callSession = CallSession.getInstance() +val session = CometChatCalls.getActiveSession() -// Listen for end call button click -callSession.addButtonClickListener(this, object : ButtonClickListener() { - override fun onLeaveSessionButtonClicked() { - endCall(currentSessionId) - } - // Other callbacks... +// The default call UI's End button has already left when this fires; only clean up. +session?.addButtonClickListener(this, object : ButtonClickListener() { + override fun onLeaveSessionButtonClicked() = finish() }) -fun endCall(sessionId: String) { - // 1. Leave the call session (Calls SDK) - CallSession.getInstance().leaveSession() - - // 2. Notify other participants (Chat SDK) - CometChat.endCall(sessionId, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call ended successfully") - finish() - } - - override fun onError(e: CometChatException) { - Log.e(TAG, "End call failed: ${e.message}") - finish() - } - }) +// Your own hang-up control: +fun hangUp() { + CometChatCalls.getActiveSession()?.leave() + finish() } ``` ```java -CallSession callSession = CallSession.getInstance(); +CallSession session = CometChatCalls.getActiveSession(); -// Listen for end call button click -callSession.addButtonClickListener(this, new ButtonClickListener() { - @Override - public void onLeaveSessionButtonClicked() { - endCall(currentSessionId); - } - // Other callbacks... -}); - -void endCall(String sessionId) { - // 1. Leave the call session (Calls SDK) - CallSession.getInstance().leaveSession(); - - // 2. Notify other participants (Chat SDK) - CometChat.endCall(sessionId, new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call ended successfully"); - finish(); - } - - @Override - public void onError(CometChatException e) { - Log.e(TAG, "End call failed: " + e.getMessage()); - finish(); - } +// The default call UI's End button has already left when this fires; only clean up. +if (session != null) { + session.addButtonClickListener(this, new ButtonClickListener() { + @Override public void onLeaveSessionButtonClicked() { finish(); } }); } + +// Your own hang-up control: +void hangUp() { + CallSession active = CometChatCalls.getActiveSession(); + if (active != null) active.leave(); + finish(); +} ``` -The other participant receives `onCallEndedMessageReceived` callback and should leave the session: +The other participant: ```kotlin -CometChat.addCallListener(listenerID, object : CometChat.CallListener() { - override fun onCallEndedMessageReceived(call: Call) { - CallSession.getInstance().leaveSession() - finish() - } - // Other callbacks... +session.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionLeft() = finish() + override fun onConnectionClosed() = finish() }) ``` ```java -CometChat.addCallListener(listenerID, new CometChat.CallListener() { - @Override - public void onCallEndedMessageReceived(Call call) { - CallSession.getInstance().leaveSession(); - finish(); - } - // Other callbacks... +session.addSessionStatusListener(this, new SessionStatusListener() { + @Override public void onSessionLeft() { finish(); } + @Override public void onConnectionClosed() { finish(); } }); ``` -## Call Status Values - -| Status | Description | -|--------|-------------| -| `initiated` | Call has been initiated but not yet answered | -| `ongoing` | Call is currently in progress | -| `busy` | Receiver is busy on another call | -| `rejected` | Receiver rejected the call | -| `cancelled` | Caller cancelled before receiver answered | -| `ended` | Call ended normally | -| `missed` | Receiver didn't answer in time | -| `unanswered` | Call was not answered | + +`CallSession.leaveSession()` is deprecated in 5.1; use `leave()`. Leave only the session your screen owns: after answering a second call over a live one, the first screen's clean-up must not leave the new session. + + +## Second call while in a call + +Handled by the Push SDK, the same way a phone does it. A call that arrives while the user is in another call rings over it with "Answering ends your current call". **Answer** leaves the live call, prepares the new one and opens your answer activity again (with `FLAG_ACTIVITY_SINGLE_TOP`, so it arrives in `onNewIntent` if the activity is still up: open a fresh screen for the new session). **Decline** sends `busy` and keeps the live call. A call that arrives while another is already ringing, or while the user's own outgoing call is ringing, is declined as `busy` without ringing. + +## Group calls + +`initiateCall` is for 1:1 calls. A group call is a session every member joins with `joinSession` using a session id you share with them (for example in a custom message). If a group call rings through the push, declining it or letting it ring out sends nothing: the call carries on for the other members, and the decline is local to that device. + +## Logout + +Log out of the Calls SDK when the user signs out (`CometChatCalls.logout`, or `CometChatUIKit.logout` when you use the UI Kit). On that logout the Push SDK stops a call still ringing for the old user and unregisters the push token. Logging out of the Chat SDK alone leaves the Calls SDK signed in, so a ring keeps playing for the old user and later declines go out with the old token. + +End or cancel any call of your own before logging out; the Calls SDK cannot send after the session is gone. + +## Call status values + +| Status | Meaning | Sent by | +|--------|---------|---------| +| `initiated` | Created, ringing | Backend, on `initiateCall` | +| `ongoing` | Answered | Backend, when the callee joins | +| `rejected` | Callee declined | Callee's Push SDK | +| `busy` | Callee was in another call | Callee's Push SDK | +| `cancelled` | Caller ended it before an answer | Caller, `endCall` | +| `unanswered` | Nobody answered in the ring window | Callee's Push SDK (and optionally the caller's timer) | +| `ended` | Hung up after being answered | Backend, when the session ends | +| `missed` | How an unanswered or cancelled incoming call appears in call logs | Call logs | + +## Related + +- [Push ringing](/calls/android/voip-calling): dependencies, `PNConfiguration`, the FCM service, answer activity, your own incoming-call screen, readiness checks and what happens when the app is killed. +- [Migrating from legacy ringing](/calls/android/migrating-from-legacy-ringing). +- [Events](/calls/android/events), [Actions](/calls/android/actions), [Call Logs](/calls/android/call-logs). diff --git a/calls/android/setup.mdx b/calls/android/setup.mdx index bfea11cc1..7f08ea651 100644 --- a/calls/android/setup.mdx +++ b/calls/android/setup.mdx @@ -2,7 +2,7 @@ title: "Setup" sidebarTitle: "Setup" sdk_version: "5.x" -description: "Set up CometChat Calls SDK v5 for Android with installation, app credentials, initialization, authentication, and a basic calling flow." +description: "Set up CometChat Calls SDK v5 for Android with installation, app credentials, initialization, host overrides, authentication, and the push dependencies needed for ringing." --- @@ -59,7 +59,7 @@ Add the Calls SDK dependency to your **app level** `build.gradle` file: ```groovy dependencies { - implementation "com.cometchat:calls-sdk-android:5.0.4" + implementation "com.cometchat:calls-sdk-android:5.1.0" // Required. The Calls SDK renders its call surface through an embedded React Native // runtime, and your app must initialize its native loader (Step 4) - which only @@ -72,7 +72,7 @@ dependencies { ```kotlin dependencies { - implementation("com.cometchat:calls-sdk-android:5.0.4") + implementation("com.cometchat:calls-sdk-android:5.1.0") // Required. The Calls SDK renders its call surface through an embedded React Native // runtime, and your app must initialize its native loader (Step 4) - which only @@ -84,6 +84,13 @@ dependencies { + + **Ringing users?** Incoming calls ring through the Push Notifications SDK's calls module. Add + `com.cometchat:push-notifications-android:2.0.0` and `com.cometchat:push-notifications-calls-android:2.0.0` + as well, and follow [Push Ringing](/calls/android/voip-calling). The Calls SDK alone can start, join and end + calls, but it cannot ring a device that is not running your app. + + **Set `minSdkVersion` to 26 or higher.** The Calls SDK's transitive dependencies declare `minSdk 26`, so a lower value fails the build at the manifest merger: @@ -191,7 +198,9 @@ Add the required permissions to your `AndroidManifest.xml`: ``` -For Android 6.0 (API level 23) and above, you must request camera and microphone permissions at runtime before starting a call. +Request camera and microphone permissions at runtime before starting or joining a call. When a call is answered from +the Push SDK's ring, the SDK asks for them before opening your answer activity; a denied microphone joins muted. On +Android 14+ the call's foreground service is blocked while the microphone permission is missing. ## Initialize CometChat Calls @@ -206,6 +215,9 @@ The `CallAppSettings` class configures the SDK initialization: |-----------|------|----------|-------------| | `appId` | String | Yes | Your CometChat App ID | | `region` | String | Yes | Your app region (`us` or `eu`) | +| `overrideAdminHost(host)` | String | No | Admin API host of a staging or dedicated deployment | +| `overrideClientHost(host)` | String | No | Client API host of that deployment; the same value the Chat SDK and the Push SDK get | +| `overrideCallsHost(host)` | String | No | Calls API host of that deployment, where call tokens are issued | @@ -260,6 +272,38 @@ CometChatCalls.init(this, callAppSettings, new CometChatCalls.CallbackListener + **Initialize once, with the final hosts.** The SDK keeps the hosts from its first `init()`; a later `init()` with + different hosts is ignored for the rest of the process. If you use the CometChat UI Kit with `enableCalling`, it + initializes the Calls SDK for you with the hosts from `UIKitSettings`, so do not call `init()` again yourself. + + +### Staging or dedicated deployments + +Pass the deployment's hosts with the override methods. All three are needed: the Push SDK and the Chat SDK use the +client host, and call tokens come from the calls host. + + + +```kotlin +val callAppSettings = CallAppSettings.CallAppSettingBuilder(appId, region) + .overrideAdminHost("$appId.api-$region.example.com/v3") + .overrideClientHost("$appId.apiclient-$region.example.com/v3") + .overrideCallsHost("$appId.call-$region.example.com/v3") + .build() +``` + + +```java +CallAppSettings callAppSettings = new CallAppSettings.CallAppSettingBuilder(appId, region) + .overrideAdminHost(appId + ".api-" + region + ".example.com/v3") + .overrideClientHost(appId + ".apiclient-" + region + ".example.com/v3") + .overrideCallsHost(appId + ".call-" + region + ".example.com/v3") + .build(); +``` + + + ## Check Initialization Status You can verify if the SDK has been initialized using the `isInitialized()` method: diff --git a/calls/android/troubleshooting.mdx b/calls/android/troubleshooting.mdx index 8aae2b78f..1dc1d7068 100644 --- a/calls/android/troubleshooting.mdx +++ b/calls/android/troubleshooting.mdx @@ -50,7 +50,12 @@ sdk_version: "5.x" | Symptom | Cause | Fix | | --- | --- | --- | | Call drops when app goes to background | No foreground service | Implement a foreground service to keep the call alive. See [Background Handling](/calls/android/background-handling) | -| VoIP notifications not working | FCM not configured | Set up Firebase Cloud Messaging. See [VoIP Calling](/calls/android/voip-calling) | +| Incoming calls do not ring | Push Notifications SDK calls module missing, push not handed to `handlePushNotification`, or the device is not ring-ready | Add `push-notifications-calls-android`, pass every push with `sentTimeMs`, run `ensureRingReady`. See [Push Ringing](/calls/android/voip-calling) | +| Rings, then the process dies before Answer | Call push sent at normal FCM priority (`BFGS denied`) | Send call pushes at high priority | +| No push on a staging or dedicated deployment | Push SDK registered the token on the default host | Pass the same `overrideClientHost` to `PNConfiguration` as to the Chat and Calls SDKs | +| Caller never sees "Ringing…" / answered | Caller joined right after `initiateCall` | Stay on the prepared session and join on `onParticipantJoined`. See [Ringing](/calls/android/ringing) | +| Audio call opens the camera | Session type not set at `initiateCall` | Pass `sessionSettings` with `setSessionType(SessionType.VOICE)` in `InitiateCallOptions` | +| Ring keeps playing after logout | Only the Chat SDK was logged out | Call `CometChatCalls.logout` too (or `CometChatUIKit.logout`) | | App killed by OS during call | Missing foreground service notification | Show a persistent notification during active calls | --- diff --git a/calls/android/voip-calling.mdx b/calls/android/voip-calling.mdx index ee63514bf..02d9b1148 100644 --- a/calls/android/voip-calling.mdx +++ b/calls/android/voip-calling.mdx @@ -1,2152 +1,571 @@ --- -title: "VoIP Calling" -sidebarTitle: "VoIP Calling" +title: "Push Ringing" +sidebarTitle: "Push Ringing (VoIP)" sdk_version: "5.x" -description: "Set up CometChat Calls SDK v5 VoIP calling on Android for call notifications, incoming call handling, and app wake flows." +description: "Ring incoming CometChat calls on Android with the Push Notifications SDK 2.0 calls module: FCM setup, configuration, the answer activity, permissions, busy and missed calls, your own ring screen, and behaviour when the app is killed." --- -Implement native VoIP calling that works when your app is in the background or killed. This guide shows how to integrate Android's Telecom framework with CometChat to display system call UI, handle calls from the lock screen, and provide a native calling experience. +The Push Notifications SDK's calls module turns a CometChat call push into a ringing phone: a call-style notification with Answer and Decline, a full-screen screen over the lock screen, the ringtone, a ring timeout, missed-call notifications and busy handling. It works with the app in the foreground, in the background or not running at all, and needs no Telecom `ConnectionService`. -## Overview - -VoIP calling differs from [basic in-app ringing](/calls/android/ringing) by leveraging Android's `ConnectionService` to: -- Show incoming calls on lock screen with system UI -- Handle calls when app is in background or killed -- Integrate with Bluetooth, car systems, and wearables -- Provide consistent call experience across Android devices - -```mermaid -flowchart TB - subgraph "Incoming Call Flow" - A[FCM Push Notification] --> B[FirebaseMessagingService] - B --> C{App State?} - C -->|Background/Killed| D[ConnectionService] - C -->|Foreground| E[CometChat CallListener] - D --> F[System Call UI] - E --> G[In-App Call UI] - F --> H{User Action} - G --> H - H -->|Accept| I[CometChat.acceptCall] - H -->|Reject| J[CometChat.rejectCall] - I --> K[CometChatCalls.joinSession] - end -``` - -## Prerequisites - -Before implementing VoIP calling, ensure you have: - -- [CometChat Chat SDK](/sdk/android/overview) and [Calls SDK](/calls/android/setup) integrated -- [Firebase Cloud Messaging (FCM)](/notifications/android-push-notifications) configured -- [Push notifications enabled](/notifications/push-overview) in CometChat Dashboard -- Android 8.0+ (API 26+) for ConnectionService support - - -This documentation builds on the [Ringing](/calls/android/ringing) functionality. Make sure you understand basic call signaling before implementing VoIP. - - ---- - -## Architecture Overview - -The VoIP implementation consists of several components working together: +This page covers the push side of ringing. Starting, answering and ending calls with the Calls SDK is on the [Ringing](/calls/android/ringing) page. ```mermaid flowchart LR - subgraph "Your App" - A[FirebaseMessagingService] - B[ConnectionService] - C[CallActivity] - D[CallNotificationManager] - end - - subgraph "CometChat" - E[Chat SDK] - F[Calls SDK] - end - - subgraph "Android System" - G[Telecom Manager] - H[System Call UI] - end - - A -->|Call Push| D - D -->|Background| B - B <-->|Register| G - G --> H - H -->|Accept/Reject| B - B -->|Accept| E - E -->|Session| F - F --> C + FCM[FCM push
type=call] --> S[Your FirebaseMessagingService] + S --> H["CometChatPushNotifications.handlePushNotification()"] + H --> D{Push SDK decides} + D -->|ring| R[Notification + full-screen screen + ringtone] + D -->|in another call| W[Rings over the live call] + D -->|another call ringing| B[Declined as busy] + R -->|Answer| P[Mic / camera prompt] + P --> A[Your answer activity → joinSession] + R -->|Decline| Q["endCall(rejected) via durable queue"] + R -->|45 s| T["endCall(unanswered) + missed call"] ``` -| Component | Purpose | -|-----------|---------| -| `FirebaseMessagingService` | Receives push notifications for incoming calls when app is in background | -| `ConnectionService` | Android Telecom framework integration - manages call state with the system | -| `CallNotificationManager` | Decides whether to show system call UI or fallback notification | -| `PhoneAccount` | Registers your app as a calling app with Android's Telecom system | -| `Connection` | Represents an individual call and handles user actions (accept/reject/hold) | - ---- - -## Step 1: Configure Push Notifications - -Push notifications are essential for receiving incoming calls when your app is not in the foreground. When a call is initiated, CometChat sends a push notification to the receiver's device. - - -For detailed FCM setup instructions, see the [Android Push Notifications](/notifications/android-push-notifications) documentation. - +## Prerequisites -### 1.1 Add FCM Dependencies +- [Calls SDK 5.1 set up](/calls/android/setup) and the user logged in. +- A Firebase project with your Android app, `google-services.json` in the app module, and the Firebase Messaging dependency. The Push SDK never compiles against Firebase; you own it. +- Push notifications enabled in the CometChat dashboard with an **FCM** provider, and that provider's id. +- `minSdk 26`. -Add Firebase Messaging to your `build.gradle`: +## Step 1: Add the dependencies -```groovy +```kotlin build.gradle.kts dependencies { - implementation 'com.google.firebase:firebase-messaging:23.4.0' + implementation("com.cometchat:calls-sdk-android:5.1.0") + implementation("com.cometchat:push-notifications-android:2.0.0") // routing, tokens, chat notifications + implementation("com.cometchat:push-notifications-calls-android:2.0.0") // the ringer + implementation(platform("com.google.firebase:firebase-bom:33.7.0")) + implementation("com.google.firebase:firebase-messaging") } ``` -### 1.2 Create FirebaseMessagingService +Adding the calls artifact is the whole opt-in: core detects it and installs the ringer. The Chat SDK is optional for both artifacts; a calls-only app works. -This service receives push notifications from FCM. When a call notification arrives, it extracts the call data and decides how to display the incoming call based on the app's state. +## Step 2: Initialise once +Run this once per install, for example in `Application.onCreate`. The configuration is saved to disk, so a push that wakes a cold process still finds it. ```kotlin -class CallFirebaseMessagingService : FirebaseMessagingService() { - - override fun onMessageReceived(remoteMessage: RemoteMessage) { - val data = remoteMessage.data - - // Check if this is a call notification by examining the "type" field - // CometChat sends call notifications with type="call" - if (data["type"] == "call") { - handleIncomingCall(data) - } - } - - private fun handleIncomingCall(data: Map) { - // Extract call information from the push payload - // These fields are sent by CometChat when a call is initiated - val sessionId = data["sessionId"] ?: return - val callerName = data["senderName"] ?: "Unknown" - val callerUid = data["senderUid"] ?: return - val callType = data["callType"] ?: "video" // "audio" or "video" - val callerAvatar = data["senderAvatar"] - - // Create a CallData object to pass call information between components - val callData = CallData( - sessionId = sessionId, - callerName = callerName, - callerUid = callerUid, - callType = callType, - callerAvatar = callerAvatar - ) - - // If app is in foreground, let CometChat's CallListener handle it - // This provides a seamless experience with in-app UI - if (isAppInForeground()) { - return - } - - // App is in background/killed - show system call UI via ConnectionService - // This ensures the user sees the incoming call even when not using the app - CallNotificationManager.showIncomingCall(this, callData) - } - - /** - * Checks if the app is currently visible to the user. - * We only want to use ConnectionService when the app is in background. - */ - private fun isAppInForeground(): Boolean { - val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val appProcesses = activityManager.runningAppProcesses ?: return false - val packageName = packageName - for (appProcess in appProcesses) { - if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND - && appProcess.processName == packageName) { - return true - } - } - return false - } - - /** - * Called when FCM generates a new token. - * Register this token with CometChat to receive push notifications. - */ - override fun onNewToken(token: String) { - // Register the FCM token with CometChat's push notification service - // This links the device to the logged-in user for push delivery - CometChat.registerTokenForPushNotification(token, object : CometChat.CallbackListener() { - override fun onSuccess(s: String) { - Log.d(TAG, "Push token registered successfully") - } - override fun onError(e: CometChatException) { - Log.e(TAG, "Token registration failed: ${e.message}") - } - }) - } - - companion object { - private const val TAG = "CallFCMService" - } -} -``` - - -```java -public class CallFirebaseMessagingService extends FirebaseMessagingService { - - private static final String TAG = "CallFCMService"; - - @Override - public void onMessageReceived(RemoteMessage remoteMessage) { - Map data = remoteMessage.getData(); - - // Check if this is a call notification by examining the "type" field - // CometChat sends call notifications with type="call" - if ("call".equals(data.get("type"))) { - handleIncomingCall(data); - } - } - - private void handleIncomingCall(Map data) { - // Extract call information from the push payload - // These fields are sent by CometChat when a call is initiated - String sessionId = data.get("sessionId"); - String callerName = data.get("senderName"); - String callerUid = data.get("senderUid"); - String callType = data.get("callType"); - String callerAvatar = data.get("senderAvatar"); - - if (sessionId == null || callerUid == null) return; - if (callerName == null) callerName = "Unknown"; - if (callType == null) callType = "video"; - - // Create a CallData object to pass call information between components - CallData callData = new CallData( - sessionId, callerName, callerUid, callType, callerAvatar - ); - - // If app is in foreground, let CometChat's CallListener handle it - if (isAppInForeground()) { - return; - } - - // App is in background/killed - show system call UI via ConnectionService - CallNotificationManager.showIncomingCall(this, callData); - } - - /** - * Checks if the app is currently visible to the user. - * We only want to use ConnectionService when the app is in background. - */ - private boolean isAppInForeground() { - ActivityManager activityManager = - (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - List appProcesses = - activityManager.getRunningAppProcesses(); - if (appProcesses == null) return false; - - String packageName = getPackageName(); - for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { - if (appProcess.importance == - ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND - && appProcess.processName.equals(packageName)) { - return true; - } - } - return false; - } - - /** - * Called when FCM generates a new token. - * Register this token with CometChat to receive push notifications. - */ - @Override - public void onNewToken(String token) { - // Register the FCM token with CometChat's push notification service - CometChat.registerTokenForPushNotification(token, new CometChat.CallbackListener() { - @Override - public void onSuccess(String s) { - Log.d(TAG, "Push token registered successfully"); - } - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Token registration failed: " + e.getMessage()); - } - }); - } -} -``` - - - -### 1.3 Create CallData Model - -The `CallData` class is a simple data container that holds all information about an incoming or outgoing call. It implements `Parcelable` so it can be passed between Android components (Activities, Services, BroadcastReceivers). - - - -```kotlin -/** - * Data class representing call information. - * Implements Parcelable to allow passing between Android components. - */ -data class CallData( - val sessionId: String, // Unique identifier for the call session - val callerName: String, // Display name of the caller - val callerUid: String, // CometChat UID of the caller - val callType: String, // "audio" or "video" - val callerAvatar: String? // URL to caller's avatar image (optional) -) : Parcelable { - - constructor(parcel: Parcel) : this( - parcel.readString() ?: "", - parcel.readString() ?: "", - parcel.readString() ?: "", - parcel.readString() ?: "", - parcel.readString() - ) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(sessionId) - parcel.writeString(callerName) - parcel.writeString(callerUid) - parcel.writeString(callType) - parcel.writeString(callerAvatar) - } - - override fun describeContents(): Int = 0 - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): CallData = CallData(parcel) - override fun newArray(size: Int): Array = arrayOfNulls(size) - } -} +CometChatPushNotifications.init( + this, + PNConfiguration.Builder(APP_ID, REGION) + .setProviderId(PROVIDER_ID) // FCM provider id from the dashboard + .setAnswerActivity(CallScreenActivity::class.java) // opened on Answer, call in its intent + .setNotificationSmallIcon(R.drawable.ic_notification) + .setInlineReplyEnabled(true) // chat notifications only + .build() +) ``` ```java -/** - * Data class representing call information. - * Implements Parcelable to allow passing between Android components. - */ -public class CallData implements Parcelable { - private final String sessionId; // Unique identifier for the call session - private final String callerName; // Display name of the caller - private final String callerUid; // CometChat UID of the caller - private final String callType; // "audio" or "video" - private final String callerAvatar; // URL to caller's avatar image (optional) - - public CallData(String sessionId, String callerName, String callerUid, - String callType, String callerAvatar) { - this.sessionId = sessionId; - this.callerName = callerName; - this.callerUid = callerUid; - this.callType = callType; - this.callerAvatar = callerAvatar; - } - - // Getters - public String getSessionId() { return sessionId; } - public String getCallerName() { return callerName; } - public String getCallerUid() { return callerUid; } - public String getCallType() { return callType; } - public String getCallerAvatar() { return callerAvatar; } - - // Parcelable implementation for passing between components - protected CallData(Parcel in) { - sessionId = in.readString(); - callerName = in.readString(); - callerUid = in.readString(); - callType = in.readString(); - callerAvatar = in.readString(); - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(sessionId); - dest.writeString(callerName); - dest.writeString(callerUid); - dest.writeString(callType); - dest.writeString(callerAvatar); - } - - @Override - public int describeContents() { return 0; } - - public static final Creator CREATOR = new Creator() { - @Override - public CallData createFromParcel(Parcel in) { return new CallData(in); } - @Override - public CallData[] newArray(int size) { return new CallData[size]; } - }; -} +CometChatPushNotifications.init( + this, + new PNConfiguration.Builder(APP_ID, REGION) + .setProviderId(PROVIDER_ID) // FCM provider id from the dashboard + .setAnswerActivity(CallScreenActivity.class) // opened on Answer, call in its intent + .setNotificationSmallIcon(R.drawable.ic_notification) + .setInlineReplyEnabled(true) // chat notifications only + .build() +); ``` ---- +A calls-only app can start from the settings it already has: `PNConfiguration.from(callAppSettings)` in Kotlin, `CometChatCallPushConfig.from(callAppSettings)` in Java. -## Step 2: Register PhoneAccount +| Setter | Default | What it does | +|--------|---------|--------------| +| `setProviderId(id)` | none | The dashboard FCM provider the token is registered under | +| `setAnswerActivity(Class)` | none, **required** | Started when the user answers, with the call in its extras | +| `setCallMode(CallMode.AUTO \| MANUAL)` | `AUTO` | `AUTO` rings. `MANUAL` only parses and forwards; see [MANUAL mode](#manual-mode) | +| `setIncomingCallActivity(Class?)` | the SDK's own screen | Your full-screen ring screen; see [Your own incoming-call screen](#your-own-incoming-call-screen) | +| `setMissedCallActivity(Class?)` | none | Opened when a missed-call notification is tapped. Without it an individual missed-call notification has no tap action | +| `setNotificationSmallIcon(res)` | none | Status-bar icon for every notification | +| `setDefaultRingTimeoutMs(ms)` | 45 000 | Ring window when the push carries no `ringTimeout`; 1 s to 10 min | +| `setShowMissedCallNotification(bool)` | `true` | Post a missed-call notification | +| `setRequestCallPermissions(bool)` | `true` | Ask for microphone (and camera) on Answer | +| `overrideClientHost(host)` | none | Same value as the Chat and Calls SDKs' `overrideClientHost`; see [Staging deployments](#staging-or-dedicated-deployments) | +| `setInlineReplyEnabled(bool)` | `true` | Reply from a chat notification (needs the Chat SDK) | +| `setTokenRegistrar(CHAT \| CALLS \| AUTO)`, `setAuthToken`, `setAutoRegisterToken` | `AUTO`, none, `true` | Token registration; leave the defaults | -A `PhoneAccount` tells Android that your app can handle phone calls. This registration is required for the system to route incoming calls to your app and display the native call UI. +`build()` throws if the calls module is present, the mode is `AUTO`, and no answer activity is set. -### 2.1 Create PhoneAccountManager - -This singleton class handles registering your app with Android's Telecom system. The `PhoneAccount` must be registered before you can receive or make VoIP calls. +## Step 3: Delegate from your Firebase service ```kotlin -/** - * Manages PhoneAccount registration with Android's Telecom system. - * Must be called once when the app starts (typically in Application.onCreate). - */ -object PhoneAccountManager { - private const val PHONE_ACCOUNT_ID = "cometchat_voip_account" - private var phoneAccountHandle: PhoneAccountHandle? = null - - /** - * Registers your app as a calling app with Android's Telecom system. - * Call this in your Application.onCreate() method. - */ - fun register(context: Context) { - val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - - // ComponentName points to your ConnectionService implementation - val componentName = ComponentName(context, CallConnectionService::class.java) - - // PhoneAccountHandle uniquely identifies your calling account - phoneAccountHandle = PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID) - - // Build the PhoneAccount with required capabilities - val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "CometChat Calls") - .setCapabilities( - // CAPABILITY_CALL_PROVIDER: Can make and receive calls - // CAPABILITY_SELF_MANAGED: Manages its own call UI (required for VoIP) - PhoneAccount.CAPABILITY_CALL_PROVIDER or - PhoneAccount.CAPABILITY_SELF_MANAGED - ) - .build() - - // Register with the system - telecomManager.registerPhoneAccount(phoneAccount) - } +class MyFirebaseService : FirebaseMessagingService() { - /** - * Returns the PhoneAccountHandle for use with TelecomManager calls. - */ - fun getPhoneAccountHandle(context: Context): PhoneAccountHandle { - if (phoneAccountHandle == null) { - val componentName = ComponentName(context, CallConnectionService::class.java) - phoneAccountHandle = PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID) - } - return phoneAccountHandle!! - } - - /** - * Checks if the user has enabled the PhoneAccount in system settings. - * Some devices require manual enabling in Settings > Apps > Phone > Calling accounts. - */ - fun isPhoneAccountEnabled(context: Context): Boolean { - val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - val account = telecomManager.getPhoneAccount(getPhoneAccountHandle(context)) - return account?.isEnabled == true + override fun onMessageReceived(message: RemoteMessage) { + val handled = CometChatPushNotifications.handlePushNotification( + context = this, + data = message.data, + sentTimeMs = message.sentTime + ) + if (!handled) { /* not a CometChat push: your own handling */ } } - /** - * Opens system settings where user can enable the PhoneAccount. - * Call this if isPhoneAccountEnabled() returns false. - */ - fun openPhoneAccountSettings(context: Context) { - val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - context.startActivity(intent) + override fun onNewToken(token: String) { + CometChatPushNotifications.handleTokenRefresh(token) } } ``` ```java -/** - * Manages PhoneAccount registration with Android's Telecom system. - * Must be called once when the app starts (typically in Application.onCreate). - */ -public class PhoneAccountManager { - private static final String PHONE_ACCOUNT_ID = "cometchat_voip_account"; - private static PhoneAccountHandle phoneAccountHandle; - - /** - * Registers your app as a calling app with Android's Telecom system. - * Call this in your Application.onCreate() method. - */ - public static void register(Context context) { - TelecomManager telecomManager = - (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); - - // ComponentName points to your ConnectionService implementation - ComponentName componentName = - new ComponentName(context, CallConnectionService.class); - - // PhoneAccountHandle uniquely identifies your calling account - phoneAccountHandle = new PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID); - - // Build the PhoneAccount with required capabilities - PhoneAccount phoneAccount = PhoneAccount.builder(phoneAccountHandle, "CometChat Calls") - .setCapabilities( - // CAPABILITY_CALL_PROVIDER: Can make and receive calls - // CAPABILITY_SELF_MANAGED: Manages its own call UI (required for VoIP) - PhoneAccount.CAPABILITY_CALL_PROVIDER | - PhoneAccount.CAPABILITY_SELF_MANAGED - ) - .build(); +public class MyFirebaseService extends FirebaseMessagingService { - // Register with the system - telecomManager.registerPhoneAccount(phoneAccount); - } - - /** - * Returns the PhoneAccountHandle for use with TelecomManager calls. - */ - public static PhoneAccountHandle getPhoneAccountHandle(Context context) { - if (phoneAccountHandle == null) { - ComponentName componentName = - new ComponentName(context, CallConnectionService.class); - phoneAccountHandle = new PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID); - } - return phoneAccountHandle; - } - - /** - * Checks if the user has enabled the PhoneAccount in system settings. - */ - public static boolean isPhoneAccountEnabled(Context context) { - TelecomManager telecomManager = - (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); - PhoneAccount account = telecomManager.getPhoneAccount(getPhoneAccountHandle(context)); - return account != null && account.isEnabled(); - } - - /** - * Opens system settings where user can enable the PhoneAccount. - */ - public static void openPhoneAccountSettings(Context context) { - Intent intent = new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); + @Override + public void onMessageReceived(@NonNull RemoteMessage message) { + boolean handled = CometChatPushNotifications.handlePushNotification( + this, message.getData(), message.getSentTime()); + if (!handled) { /* not a CometChat push: your own handling */ } } -} -``` - - - -### 2.2 Register on App Start -Register the PhoneAccount when your app starts. This should be done in your `Application` class to ensure it's registered before any calls can be received. - - - -```kotlin -class MyApplication : Application() { - override fun onCreate() { - super.onCreate() - - // Initialize CometChat (see Setup guide) - // CometChat.init(...) - - // Register PhoneAccount for VoIP calling - // This must be done before receiving any calls - PhoneAccountManager.register(this) - } -} -``` - - -```java -public class MyApplication extends Application { @Override - public void onCreate() { - super.onCreate(); - - // Initialize CometChat (see Setup guide) - // CometChat.init(...) - - // Register PhoneAccount for VoIP calling - // This must be done before receiving any calls - PhoneAccountManager.register(this); + public void onNewToken(@NonNull String token) { + CometChatPushNotifications.handleTokenRefresh(token); } } ``` ---- - -## Step 3: Implement ConnectionService +```xml AndroidManifest.xml + + + + + +``` -The `ConnectionService` is the core component that bridges your app with Android's Telecom framework. It creates `Connection` objects that represent individual calls and handle user interactions. + +**Pass `sentTimeMs` and hand over every push.** Firebase queues pushes while a device dozes; without the sent time a push delivered late looks fresh and rings for a call that is already over. And do not skip call pushes when the app is in the foreground: the push is the only signal that a call is coming in, in every app state. + -### 3.1 Create CallConnectionService + +**Send call pushes at high FCM priority.** Android lets a background app start a foreground service only with the temporary allowlist that a high-priority data message grants. At normal priority the call still rings, but the process can be killed between the ring and the answer (`BFGS denied` in logcat). + -This service is called by Android when a new incoming or outgoing call needs to be created. It's responsible for creating `Connection` objects that manage the call state. +## Step 4: Join from the answer activity +On **Answer** the SDK stops the ring, asks for the microphone (and the camera for a video call), dismisses the keyguard and starts your answer activity with the call in its extras. Read it and join; there is nothing to "accept". ```kotlin -/** - * ConnectionService implementation for VoIP calling. - * Android's Telecom framework calls this service to create Connection objects - * for incoming and outgoing calls. - */ -class CallConnectionService : ConnectionService() { - - /** - * Called by Android when an incoming call is reported via TelecomManager.addNewIncomingCall(). - * Creates a Connection object that will handle the incoming call. - */ - override fun onCreateIncomingConnection( - connectionManagerPhoneAccount: PhoneAccountHandle?, - request: ConnectionRequest? - ): Connection { - // Extract call data from the request extras - val extras = request?.extras - val callData = extras?.getParcelable(EXTRA_CALL_DATA) - - // Create a new Connection to represent this call - val connection = CallConnection(applicationContext, callData) - - // Set initial state: initializing -> ringing - // This triggers the system to show the incoming call UI - connection.setInitializing() - connection.setRinging() - - // Store the connection so we can access it from other components - CallConnectionHolder.setConnection(connection) - - return connection - } - - /** - * Called by Android when an outgoing call is placed via TelecomManager.placeCall(). - * Creates a Connection object that will handle the outgoing call. - */ - override fun onCreateOutgoingConnection( - connectionManagerPhoneAccount: PhoneAccountHandle?, - request: ConnectionRequest? - ): Connection { - val extras = request?.extras - val callData = extras?.getParcelable(EXTRA_CALL_DATA) - - val connection = CallConnection(applicationContext, callData) - - // Set initial state: initializing -> dialing - // This triggers the system to show the outgoing call UI - connection.setInitializing() - connection.setDialing() - - CallConnectionHolder.setConnection(connection) - - return connection - } +class CallScreenActivity : AppCompatActivity() { - /** - * Called when the system fails to create an incoming connection. - * This can happen due to permission issues or system constraints. - */ - override fun onCreateIncomingConnectionFailed( - connectionManagerPhoneAccount: PhoneAccountHandle?, - request: ConnectionRequest? - ) { - Log.e(TAG, "Failed to create incoming connection") - // Consider showing a fallback notification here - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + setShowWhenLocked(true) + setTurnScreenOn(true) - /** - * Called when the system fails to create an outgoing connection. - */ - override fun onCreateOutgoingConnectionFailed( - connectionManagerPhoneAccount: PhoneAccountHandle?, - request: ConnectionRequest? - ) { - Log.e(TAG, "Failed to create outgoing connection") + val call = CallBundles.getCall(intent) ?: return finish() + val settings = CometChatCalls.SessionSettingsBuilder() + .setSessionType(if (call.isVideo) SessionType.VIDEO else SessionType.VOICE) + .build() + CometChatCalls.joinSession(call.sessionId, settings, findViewById(R.id.call_view_container), + object : CometChatCalls.CallbackListener() { + override fun onSuccess(session: CallSession) {} + override fun onError(e: CometChatException) = finish() + }) } - companion object { - private const val TAG = "CallConnectionService" - const val EXTRA_CALL_DATA = "extra_call_data" + // A second call answered over this one arrives here (SINGLE_TOP): open a fresh screen for it. + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + val next = CallBundles.getCall(intent) ?: return + if (next.sessionId != CallBundles.getCall(getIntent())?.sessionId) { + startActivity(Intent(intent).setClass(this, CallScreenActivity::class.java)) + finish() + } } } ``` ```java -/** - * ConnectionService implementation for VoIP calling. - * Android's Telecom framework calls this service to create Connection objects - * for incoming and outgoing calls. - */ -public class CallConnectionService extends ConnectionService { - - private static final String TAG = "CallConnectionService"; - public static final String EXTRA_CALL_DATA = "extra_call_data"; - - /** - * Called by Android when an incoming call is reported via TelecomManager.addNewIncomingCall(). - * Creates a Connection object that will handle the incoming call. - */ - @Override - public Connection onCreateIncomingConnection( - PhoneAccountHandle connectionManagerPhoneAccount, - ConnectionRequest request) { - - // Extract call data from the request extras - Bundle extras = request.getExtras(); - CallData callData = extras.getParcelable(EXTRA_CALL_DATA); - - // Create a new Connection to represent this call - CallConnection connection = new CallConnection(getApplicationContext(), callData); - - // Set initial state: initializing -> ringing - // This triggers the system to show the incoming call UI - connection.setInitializing(); - connection.setRinging(); - - // Store the connection so we can access it from other components - CallConnectionHolder.setConnection(connection); - - return connection; - } +public class CallScreenActivity extends AppCompatActivity { - /** - * Called by Android when an outgoing call is placed via TelecomManager.placeCall(). - * Creates a Connection object that will handle the outgoing call. - */ @Override - public Connection onCreateOutgoingConnection( - PhoneAccountHandle connectionManagerPhoneAccount, - ConnectionRequest request) { - - Bundle extras = request.getExtras(); - CallData callData = extras.getParcelable(EXTRA_CALL_DATA); - - CallConnection connection = new CallConnection(getApplicationContext(), callData); - - // Set initial state: initializing -> dialing - connection.setInitializing(); - connection.setDialing(); - - CallConnectionHolder.setConnection(connection); - - return connection; - } + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + setShowWhenLocked(true); + setTurnScreenOn(true); - @Override - public void onCreateIncomingConnectionFailed( - PhoneAccountHandle connectionManagerPhoneAccount, - ConnectionRequest request) { - Log.e(TAG, "Failed to create incoming connection"); + IncomingCall call = CallBundles.getCall(getIntent()); + if (call == null) { finish(); return; } + SessionSettings settings = new CometChatCalls.SessionSettingsBuilder() + .setSessionType(call.isVideo() ? SessionType.VIDEO : SessionType.VOICE) + .build(); + CometChatCalls.joinSession(call.getSessionId(), settings, findViewById(R.id.call_view_container), + new CometChatCalls.CallbackListener() { + @Override public void onSuccess(CallSession session) {} + @Override public void onError(CometChatException e) { finish(); } + }); } + // A second call answered over this one arrives here (SINGLE_TOP): open a fresh screen for it. @Override - public void onCreateOutgoingConnectionFailed( - PhoneAccountHandle connectionManagerPhoneAccount, - ConnectionRequest request) { - Log.e(TAG, "Failed to create outgoing connection"); + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + IncomingCall next = CallBundles.getCall(intent); + IncomingCall current = CallBundles.getCall(getIntent()); + if (next != null && (current == null || !next.getSessionId().equals(current.getSessionId()))) { + startActivity(new Intent(intent).setClass(this, CallScreenActivity.class)); + finish(); + } } } ``` -### 3.2 Create CallConnection - -The `Connection` class represents an individual call. It receives callbacks from Android when the user interacts with the call (answer, reject, hold, etc.) and is responsible for updating the call state and communicating with CometChat. - - - -```kotlin -/** - * Represents an individual VoIP call. - * Handles user actions (answer, reject, disconnect) and bridges to CometChat SDK. - */ -class CallConnection( - private val context: Context, - private val callData: CallData? -) : Connection() { - - init { - // PROPERTY_SELF_MANAGED: We manage our own call UI (not using system dialer) - connectionProperties = PROPERTY_SELF_MANAGED - - // Set capabilities for this call - // CAPABILITY_MUTE: User can mute the call - // CAPABILITY_SUPPORT_HOLD/CAPABILITY_HOLD: User can put call on hold - connectionCapabilities = CAPABILITY_MUTE or - CAPABILITY_SUPPORT_HOLD or - CAPABILITY_HOLD - - // Set caller information for the system call UI - callData?.let { - // Display name shown on incoming call screen - setCallerDisplayName(it.callerName, TelecomManager.PRESENTATION_ALLOWED) - // Address (used for call log and display) - setAddress( - Uri.parse("tel:${it.callerUid}"), - TelecomManager.PRESENTATION_ALLOWED - ) - } - - // Mark this as a VoIP call for proper audio routing - audioModeIsVoip = true - } - - /** - * Called when user taps "Answer" on the incoming call screen. - * Accept the call via CometChat and launch the call activity. - */ - override fun onAnswer() { - Log.d(TAG, "Call answered by user") - - // Update connection state to active (call is now connected) - setActive() - - callData?.let { data -> - // Accept the call via CometChat Chat SDK - // This notifies the caller that we've accepted - CometChat.acceptCall(data.sessionId, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "CometChat call accepted successfully") - // Launch the call activity to show the video/audio UI - launchCallActivity(data) - } - - override fun onError(e: CometChatException) { - Log.e(TAG, "Failed to accept call: ${e.message}") - // Call failed - disconnect and clean up - setDisconnected(DisconnectCause(DisconnectCause.ERROR)) - destroy() - } - }) - } - } - - /** - * Called when user taps "Decline" on the incoming call screen. - * Reject the call via CometChat. - */ - override fun onReject() { - Log.d(TAG, "Call rejected by user") - - callData?.let { data -> - // Reject the call via CometChat Chat SDK - // This notifies the caller that we've declined - CometChat.rejectCall( - data.sessionId, - CometChatConstants.CALL_STATUS_REJECTED, - object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call rejected successfully") - } - override fun onError(e: CometChatException) { - Log.e(TAG, "Failed to reject call: ${e.message}") - } - } - ) - } - - // Update connection state and clean up - setDisconnected(DisconnectCause(DisconnectCause.REJECTED)) - destroy() - } +`IncomingCall` fields: `sessionId`, `callerUid`, `callerName`, `callerAvatarUrl`, `callType`, `receiverType`, `receiverUid`, `conversationId`, `initiatedAtMs`, `ringTimeoutMs`, `source` (`PUSH`; `WEBSOCKET` is reserved for web), `extras`, plus the derived `isVideo`, `isPeerCall`, `initials` and `expiresAtMs`. The ring window is anchored to the caller's `initiatedAtMs`, not to a timer on this device, so both sides stop ringing together however late the push arrived; prefer `expiresAtMs` to a timer of your own. - /** - * Called when user ends the call (taps end call button). - * Leave the call session and notify the other participant. - */ - override fun onDisconnect() { - Log.d(TAG, "Call disconnected") - - // Leave the Calls SDK session if active - if (CallSession.getInstance().isSessionActive) { - CallSession.getInstance().leaveSession() - } - - // End the call via CometChat Chat SDK - // This notifies the other participant that the call has ended - callData?.let { data -> - CometChat.endCall(data.sessionId, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call ended successfully") - } - override fun onError(e: CometChatException) { - Log.e(TAG, "Failed to end call: ${e.message}") - } - }) - } - - setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) - destroy() - } - - /** - * Called when user puts the call on hold. - */ - override fun onHold() { - setOnHold() - // Mute audio when on hold - CallSession.getInstance().muteAudio() - } +Declare the activity so it can show over the lock screen: - /** - * Called when user takes the call off hold. - */ - override fun onUnhold() { - setActive() - CallSession.getInstance().unmuteAudio() - } - - /** - * Launches the CallActivity to show the call UI. - */ - private fun launchCallActivity(callData: CallData) { - val intent = Intent(context, CallActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - putExtra(CallActivity.EXTRA_SESSION_ID, callData.sessionId) - putExtra(CallActivity.EXTRA_CALL_TYPE, callData.callType) - putExtra(CallActivity.EXTRA_IS_INCOMING, true) - } - context.startActivity(intent) - } - - /** - * Public method to end the call from outside this class. - */ - fun endCall() { - onDisconnect() - } - - companion object { - private const val TAG = "CallConnection" - } -} +```xml AndroidManifest.xml + ``` - - -```java -/** - * Represents an individual VoIP call. - * Handles user actions (answer, reject, disconnect) and bridges to CometChat SDK. - */ -public class CallConnection extends Connection { - - private static final String TAG = "CallConnection"; - private final Context context; - private final CallData callData; - - public CallConnection(Context context, CallData callData) { - this.context = context; - this.callData = callData; - - // PROPERTY_SELF_MANAGED: We manage our own call UI - setConnectionProperties(PROPERTY_SELF_MANAGED); - - // Set capabilities for this call - setConnectionCapabilities( - CAPABILITY_MUTE | CAPABILITY_SUPPORT_HOLD | CAPABILITY_HOLD - ); - - // Set caller information for the system call UI - if (callData != null) { - setCallerDisplayName(callData.getCallerName(), TelecomManager.PRESENTATION_ALLOWED); - setAddress( - Uri.parse("tel:" + callData.getCallerUid()), - TelecomManager.PRESENTATION_ALLOWED - ); - } - - // Mark this as a VoIP call for proper audio routing - setAudioModeIsVoip(true); - } - - /** - * Called when user taps "Answer" on the incoming call screen. - */ - @Override - public void onAnswer() { - Log.d(TAG, "Call answered by user"); - setActive(); - - if (callData != null) { - // Accept the call via CometChat Chat SDK - CometChat.acceptCall(callData.getSessionId(), new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "CometChat call accepted successfully"); - launchCallActivity(callData); - } - - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Failed to accept call: " + e.getMessage()); - setDisconnected(new DisconnectCause(DisconnectCause.ERROR)); - destroy(); - } - }); - } - } - - /** - * Called when user taps "Decline" on the incoming call screen. - */ - @Override - public void onReject() { - Log.d(TAG, "Call rejected by user"); - - if (callData != null) { - CometChat.rejectCall( - callData.getSessionId(), - CometChatConstants.CALL_STATUS_REJECTED, - new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call rejected successfully"); - } - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Failed to reject call: " + e.getMessage()); - } - } - ); - } - - setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); - destroy(); - } - - /** - * Called when user ends the call. - */ - @Override - public void onDisconnect() { - Log.d(TAG, "Call disconnected"); - - if (CallSession.getInstance().isSessionActive()) { - CallSession.getInstance().leaveSession(); - } - if (callData != null) { - CometChat.endCall(callData.getSessionId(), new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call ended successfully"); - } - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Failed to end call: " + e.getMessage()); - } - }); - } - - setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); - destroy(); - } - - @Override - public void onHold() { - setOnHold(); - CallSession.getInstance().muteAudio(); - } + +The notification stays as a silent "Connecting call…" entry until the Calls SDK reports the session joined (60 s at most). Once joined, any in-call notification is yours to show; the SDK posts none. For a foreground service during the call see [Background Handling](/calls/android/background-handling). + - @Override - public void onUnhold() { - setActive(); - CallSession.getInstance().unmuteAudio(); - } +## Step 5: Permissions and ring readiness - private void launchCallActivity(CallData callData) { - Intent intent = new Intent(context, CallActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(CallActivity.EXTRA_SESSION_ID, callData.getSessionId()); - intent.putExtra(CallActivity.EXTRA_CALL_TYPE, callData.getCallType()); - intent.putExtra(CallActivity.EXTRA_IS_INCOMING, true); - context.startActivity(intent); - } +Declare in the manifest what the SDKs only request: - public void endCall() { - onDisconnect(); - } -} +```xml AndroidManifest.xml + + + ``` - - -### 3.3 Create CallConnectionHolder +Everything else (full-screen intent, foreground service of type `phoneCall`, wake lock, vibrate, the ring activity, the answer trampoline and the ring service) comes from the calls module's own manifest. No `MANAGE_OWN_CALLS`, `READ_PHONE_STATE` or `ANSWER_PHONE_CALLS`. + +**Microphone and camera.** The SDK asks at the moment of answering: the answer is recorded first so the ring stops, the system prompt appears, and your answer activity opens whatever the user chose. A denied microphone joins muted and never fails the call. Ask earlier as well (at sign-in, for example) so the first call is not where the user meets the prompt, and give your call screen a way to the app's settings page for a permission denied for good. If your app prompts itself, turn the SDK prompt off with `setRequestCallPermissions(false)`. -This singleton holds a reference to the active `Connection` so it can be accessed from other components (like the CallActivity or BroadcastReceiver). +**Ring readiness.** Several device settings silence a call with no error anywhere. Check once after sign-in and tell the user: ```kotlin -/** - * Singleton to hold the active CallConnection. - * Allows other components to access and control the current call. - */ -object CallConnectionHolder { - private var connection: CallConnection? = null - - fun setConnection(conn: CallConnection?) { - connection = conn - } - - fun getConnection(): CallConnection? = connection - - /** - * Ends the current call and clears the reference. - */ - fun endCall() { - connection?.endCall() - connection = null +CometChatPushNotifications.ensureRingReady(this) { result -> + val readiness = result.valueOrNull() ?: return@ensureRingReady + if (readiness != RingReadiness.READY) { + showBanner("Incoming calls may not ring: $readiness") + CometChatPushNotifications.settingsIntentFor(this, readiness)?.let(::startActivity) } - - fun hasActiveConnection(): Boolean = connection != null } ``` ```java -/** - * Singleton to hold the active CallConnection. - * Allows other components to access and control the current call. - */ -public class CallConnectionHolder { - private static CallConnection connection; - - public static void setConnection(CallConnection conn) { - connection = conn; - } - - public static CallConnection getConnection() { - return connection; - } - - public static void endCall() { - if (connection != null) { - connection.endCall(); - connection = null; - } - } - - public static boolean hasActiveConnection() { - return connection != null; - } -} +CometChatPushNotifications.ensureRingReady(this, result -> { + RingReadiness readiness = result.valueOrNull(); + if (readiness != null && readiness != RingReadiness.READY) { + showBanner("Incoming calls may not ring: " + readiness); + Intent fix = CometChatPushNotifications.settingsIntentFor(this, readiness); + if (fix != null) startActivity(fix); + } +}); ``` ---- +| Result | Meaning | +|--------|---------| +| `READY` | Nothing in the way | +| `NOTIFICATIONS_OFF` | The app cannot post notifications. `ensureRingReady` requests `POST_NOTIFICATIONS` itself on Android 13+ | +| `CHANNEL_MUTED` | The user muted or downgraded the incoming-call channel | +| `FSI_REVOKED` | Android 14+ revoked full-screen intents: no ring screen on a locked device, only the heads-up notification | +| `DND_BLOCKING` | Do Not Disturb is filtering | +| `MICROPHONE_DENIED` | The user refused the microphone at an answer; calls still ring but join muted | -## Step 4: Create CallNotificationManager +Call `ensureRingReady` only after any other permission request of yours has returned; Android drops a request made while another dialog is open. -This class is responsible for showing incoming calls to the user. It first tries to use the system call UI via `TelecomManager`, and falls back to a high-priority notification if that fails. +**Diagnostics for a bug report.** `getDiagnostics(context)` returns a redacted snapshot (whether a provider id exists, never what it is; never a token or payload): +| Key | Read it as | +|-----|------------| +| `hasPersistedConfig` | `false`: `init` never ran, or ran with a different app id | +| `callsModulePresent` | `false`: the calls artifact is not on the classpath, nothing will ring | +| `answerActivityResolves` | `false`: the configured answer activity cannot be found | +| `hasProviderId`, `firebasePresent` | `false`: token registration cannot complete | +| `canUseFullScreenIntent` | `false` on Android 14+: no ring screen on a locked device | +| `incomingChannelImportance` | below `IMPORTANCE_HIGH`: no heads-up and no sound | +| `interruptionFilter` | anything but `INTERRUPTION_FILTER_ALL`: Do Not Disturb is filtering | +| `chatSdkPresent`, `chatLoggedIn`, `callMode`, `sdkInt`, `manufacturer`, `model` | Which auth token registration can use; `MANUAL` explains a phone that never rings by design; OEM background restrictions | - - -```kotlin -/** - * Manages showing incoming calls via Android's Telecom system. - * Falls back to a high-priority notification if Telecom fails. - */ -object CallNotificationManager { - - /** - * Shows an incoming call to the user. - * Tries to use the system call UI first, falls back to notification. - */ - fun showIncomingCall(context: Context, callData: CallData) { - val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - - // Prepare extras with call data for the ConnectionService - val extras = Bundle().apply { - putParcelable(CallConnectionService.EXTRA_CALL_DATA, callData) - putParcelable( - TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, - PhoneAccountManager.getPhoneAccountHandle(context) - ) - } +`verifyPushSetup(callback)` registers the token and reports `OK`, `TOKEN_NOT_REGISTERED`, `NOT_LOGGED_IN`, `TEST_PUSH_UNAVAILABLE` (registration worked, the loopback test push is not available on the backend yet: a warning, not a failure) or `TEST_PUSH_TIMEOUT` (the test push never arrived: delivery is broken). - try { - // Tell Android there's an incoming call - // This triggers onCreateIncomingConnection in our ConnectionService - telecomManager.addNewIncomingCall( - PhoneAccountManager.getPhoneAccountHandle(context), - extras - ) - } catch (e: SecurityException) { - // Permission denied - PhoneAccount may not be enabled - Log.e(TAG, "Permission denied for incoming call: ${e.message}") - showFallbackNotification(context, callData) - } catch (e: Exception) { - Log.e(TAG, "Failed to show incoming call: ${e.message}") - showFallbackNotification(context, callData) - } - } - - /** - * Places an outgoing call via the Telecom system. - */ - fun placeOutgoingCall(context: Context, callData: CallData) { - val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - - val extras = Bundle().apply { - putParcelable(CallConnectionService.EXTRA_CALL_DATA, callData) - putParcelable( - TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, - PhoneAccountManager.getPhoneAccountHandle(context) - ) - } +Anything that takes a `PNCallback` can fail with `NOT_INITIALISED`, `NOT_LOGGED_IN`, `PROVIDER_REQUIRED`, `FCM_UNAVAILABLE`, `SERVER_ERROR` or `NETWORK_ERROR`. The token value is never logged, only its length and a six-character prefix. - try { - telecomManager.placeCall( - Uri.parse("tel:${callData.callerUid}"), - extras - ) - } catch (e: SecurityException) { - Log.e(TAG, "Permission denied for outgoing call: ${e.message}") - } - } +## How a ring ends - /** - * Shows a high-priority notification as fallback when Telecom fails. - * This notification has full-screen intent to show on lock screen. - */ - private fun showFallbackNotification(context: Context, callData: CallData) { - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) - as NotificationManager - - // Create notification channel (required for Android 8.0+) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - CHANNEL_ID, - "Incoming Calls", - NotificationManager.IMPORTANCE_HIGH // High importance for call notifications - ).apply { - description = "Notifications for incoming calls" - setSound(null, null) // We'll play our own ringtone - enableVibration(true) - } - notificationManager.createNotificationChannel(channel) - } - - // Create accept action - launches call when tapped - val acceptIntent = Intent(context, CallActionReceiver::class.java).apply { - action = ACTION_ACCEPT_CALL - putExtra(EXTRA_CALL_DATA, callData) - } - val acceptPendingIntent = PendingIntent.getBroadcast( - context, 0, acceptIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - // Create reject action - val rejectIntent = Intent(context, CallActionReceiver::class.java).apply { - action = ACTION_REJECT_CALL - putExtra(EXTRA_CALL_DATA, callData) - } - val rejectPendingIntent = PendingIntent.getBroadcast( - context, 1, rejectIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) +Every way out is handled by the SDK and needs no code in your app. - // Full screen intent - shows activity on lock screen - val fullScreenIntent = Intent(context, IncomingCallActivity::class.java).apply { - putExtra(EXTRA_CALL_DATA, callData) - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - val fullScreenPendingIntent = PendingIntent.getActivity( - context, 2, fullScreenIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) +| What happened | On the callee's device | What the caller learns | +|---------------|------------------------|------------------------| +| The user answers | Ring stops, "Connecting call…" until your screen joins | `onParticipantJoined` | +| The user declines | Ring stops; `endCall(sessionId, "rejected")` is queued and retried across process death | `onIncomingCallEnded(call, "rejected")` | +| The user declines while in another call | Same, with `"busy"` | `"busy"` | +| The caller cancels | Ring stops on the `cancelled` push, or sooner when the prepared session closes; missed-call notification | Nothing; the caller ended it | +| Nobody answers a 1:1 call | At the ring window (45 s by default, anchored to the caller's `initiatedAt`) the ring stops, a missed call is posted and `endCall(sessionId, "unanswered")` is sent | `"unanswered"` | +| Answered on another device | Ring stops on the `ongoing` push, no missed call | `onParticipantJoined` | +| Nobody answers a group call | Ring stops, missed call posted, **nothing sent**; the call keeps running | Nothing | +| The user declines a group call | Ring stops here only; the prepared session is left, nothing sent | Nothing | +| The user logs out while ringing | Ring stops, nothing sent, no missed call | Rings to its own window, then `"unanswered"` | - // Build the notification - val notification = NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_call) - .setContentTitle("Incoming ${callData.callType} call") - .setContentText("${callData.callerName} is calling...") - .setPriority(NotificationCompat.PRIORITY_MAX) - .setCategory(NotificationCompat.CATEGORY_CALL) // Marks as call notification - .setAutoCancel(true) - .setOngoing(true) // Can't be swiped away - .setFullScreenIntent(fullScreenPendingIntent, true) // Shows on lock screen - .addAction(R.drawable.ic_call_end, "Decline", rejectPendingIntent) - .addAction(R.drawable.ic_call_accept, "Accept", acceptPendingIntent) - .build() +On a decline or a 1:1 timeout the SDK does not leave the prepared session first: `endCall` announces the status to the room while still in it, then leaves, so the caller hears it at once. Once the session is prepared, the Calls SDK reporting it ended also stops the ring immediately, without waiting for a terminal push. - notificationManager.notify(NOTIFICATION_ID, notification) - } +Declines and `unanswered` go through a WorkManager queue (`cometchat-pn-decline-`): network required, exponential backoff from 10 s, given up after 5 attempts, and a call the server already ended (`ERR_CALL_TERMINATED`) counts as delivered. - /** - * Cancels the incoming call notification. - * Call this when the call is answered, rejected, or cancelled. - */ - fun cancelNotification(context: Context) { - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) - as NotificationManager - notificationManager.cancel(NOTIFICATION_ID) - } +## A call while in a call - private const val TAG = "CallNotificationManager" - private const val CHANNEL_ID = "incoming_calls" - private const val NOTIFICATION_ID = 1001 - const val ACTION_ACCEPT_CALL = "action_accept_call" - const val ACTION_REJECT_CALL = "action_reject_call" - const val EXTRA_CALL_DATA = "extra_call_data" -} -``` - - -```java -/** - * Manages showing incoming calls via Android's Telecom system. - * Falls back to a high-priority notification if Telecom fails. - */ -public class CallNotificationManager { - - private static final String TAG = "CallNotificationManager"; - private static final String CHANNEL_ID = "incoming_calls"; - private static final int NOTIFICATION_ID = 1001; - public static final String ACTION_ACCEPT_CALL = "action_accept_call"; - public static final String ACTION_REJECT_CALL = "action_reject_call"; - public static final String EXTRA_CALL_DATA = "extra_call_data"; - - /** - * Shows an incoming call to the user. - */ - public static void showIncomingCall(Context context, CallData callData) { - TelecomManager telecomManager = - (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); - - Bundle extras = new Bundle(); - extras.putParcelable(CallConnectionService.EXTRA_CALL_DATA, callData); - extras.putParcelable( - TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, - PhoneAccountManager.getPhoneAccountHandle(context) - ); - - try { - telecomManager.addNewIncomingCall( - PhoneAccountManager.getPhoneAccountHandle(context), - extras - ); - } catch (SecurityException e) { - Log.e(TAG, "Permission denied: " + e.getMessage()); - showFallbackNotification(context, callData); - } catch (Exception e) { - Log.e(TAG, "Failed to show incoming call: " + e.getMessage()); - showFallbackNotification(context, callData); - } - } +Call waiting, the way the phone app does it. "In a call" means the Calls SDK is joined to a session, or a call answered here is still joining; a session that is only prepared while it rings does not count. - /** - * Places an outgoing call via the Telecom system. - */ - public static void placeOutgoingCall(Context context, CallData callData) { - TelecomManager telecomManager = - (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); - - Bundle extras = new Bundle(); - extras.putParcelable(CallConnectionService.EXTRA_CALL_DATA, callData); - extras.putParcelable( - TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, - PhoneAccountManager.getPhoneAccountHandle(context) - ); - - try { - telecomManager.placeCall( - Uri.parse("tel:" + callData.getCallerUid()), - extras - ); - } catch (SecurityException e) { - Log.e(TAG, "Permission denied: " + e.getMessage()); - } - } +| Situation | Behaviour | +|-----------|-----------| +| Call while in a call | Rings over the live call with the normal ring; the text adds "Answering ends your current call". The live call is untouched | +| Answer (End & accept) | Leaves the live call, waits for the Calls SDK to release it, prepares the new session, opens the answer activity (`onNewIntent` if it is still up) | +| Decline | `busy` to the caller; the live call continues | +| No action | `unanswered` to the caller, missed call; the live call continues | +| Call while another one is already ringing here | Declined as `busy` without ringing, missed call. The first keeps ringing | +| Call while the user's own outgoing call is ringing | Declined as `busy` without ringing, missed call | - /** - * Shows a high-priority notification as fallback. - */ - private static void showFallbackNotification(Context context, CallData callData) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - - // Create channel for Android 8.0+ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel( - CHANNEL_ID, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH - ); - channel.setDescription("Notifications for incoming calls"); - channel.setSound(null, null); - channel.enableVibration(true); - notificationManager.createNotificationChannel(channel); - } +## Missed calls - // Accept intent - Intent acceptIntent = new Intent(context, CallActionReceiver.class); - acceptIntent.setAction(ACTION_ACCEPT_CALL); - acceptIntent.putExtra(EXTRA_CALL_DATA, callData); - PendingIntent acceptPendingIntent = PendingIntent.getBroadcast( - context, 0, acceptIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE - ); - - // Reject intent - Intent rejectIntent = new Intent(context, CallActionReceiver.class); - rejectIntent.setAction(ACTION_REJECT_CALL); - rejectIntent.putExtra(EXTRA_CALL_DATA, callData); - PendingIntent rejectPendingIntent = PendingIntent.getBroadcast( - context, 1, rejectIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE - ); - - // Full screen intent for lock screen - Intent fullScreenIntent = new Intent(context, IncomingCallActivity.class); - fullScreenIntent.putExtra(EXTRA_CALL_DATA, callData); - fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - PendingIntent fullScreenPendingIntent = PendingIntent.getActivity( - context, 2, fullScreenIntent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE - ); - - Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_call) - .setContentTitle("Incoming " + callData.getCallType() + " call") - .setContentText(callData.getCallerName() + " is calling...") - .setPriority(NotificationCompat.PRIORITY_MAX) - .setCategory(NotificationCompat.CATEGORY_CALL) - .setAutoCancel(true) - .setOngoing(true) - .setFullScreenIntent(fullScreenPendingIntent, true) - .addAction(R.drawable.ic_call_end, "Decline", rejectPendingIntent) - .addAction(R.drawable.ic_call_accept, "Accept", acceptPendingIntent) - .build(); +Posted on channel `missed_calls_v1` as "Missed call" / "Missed group call" with the caller's name and call type, one per session, grouped under the SDK's own summary ("N missed calls") so Android never folds them into your other notifications. The summary is rebuilt from the tray, so it survives the process dying. - notificationManager.notify(NOTIFICATION_ID, notification); - } +Posted when: nobody answers in the ring window; a `cancelled`, `unanswered` or `timed_out` push ends a ring here; such a push arrives for a call that never rang here (the `initiated` push was lost or overtaken); an `initiated` push arrives already expired; a call is declined as busy without ringing. - public static void cancelNotification(Context context) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(NOTIFICATION_ID); - } -} -``` - - +Not posted when: answered here or on another device; declined (`rejected` or `busy`) here; ended; the user's own outgoing call; logout while ringing. ---- +Tapping the summary opens the launcher. Tapping an individual missed call opens the activity from `setMissedCallActivity` with the call in its extras; **without that setting it does nothing**. Turn missed calls off with `setShowMissedCallNotification(false)`. This is a notification only; call history comes from [Call Logs](/calls/android/call-logs). -## Step 5: Create CallActionReceiver +## When the app is killed -This `BroadcastReceiver` handles the Accept and Decline button taps from the fallback notification. +This is the case the push exists for. - - -```kotlin -/** - * Handles notification button actions (Accept/Decline). - * Used when the fallback notification is shown instead of system call UI. - */ -class CallActionReceiver : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent) { - val callData = intent.getParcelableExtra( - CallNotificationManager.EXTRA_CALL_DATA - ) ?: return - - when (intent.action) { - CallNotificationManager.ACTION_ACCEPT_CALL -> { - acceptCall(context, callData) - } - CallNotificationManager.ACTION_REJECT_CALL -> { - rejectCall(context, callData) - } - } +1. The high-priority push starts the app process to run your Firebase service. +2. `handlePushNotification` finds no in-memory configuration and restores it from disk, then installs the calls module. +3. If the Calls SDK is not initialised yet the module initialises it with the saved hosts, and resolves the auth token (Chat SDK, then Calls SDK, then an explicit `setAuthToken`). +4. The ring runs on a foreground service of type `phoneCall`, so the system keeps the process alive through the answer. An alarm backs up the ring timeout. +5. On Answer, the trampoline activity asks for permissions and starts your answer activity, which joins. +6. A decline or timeout is sent through the WorkManager queue, so it goes out even if the process dies right after the tap. - // Always cancel the notification after handling - CallNotificationManager.cancelNotification(context) - } +Initialise the Calls SDK (and your own SDK setup) in `Application.onCreate` from stored credentials, not only after a screen opens, so the answer activity finds a ready SDK. - private fun acceptCall(context: Context, callData: CallData) { - // If we have an active Connection, use it - CallConnectionHolder.getConnection()?.onAnswer() - ?: run { - // No Connection - accept directly via CometChat - CometChat.acceptCall(callData.sessionId, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - // Launch call activity - val intent = Intent(context, CallActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - putExtra(CallActivity.EXTRA_SESSION_ID, callData.sessionId) - putExtra(CallActivity.EXTRA_CALL_TYPE, callData.callType) - } - context.startActivity(intent) - } - override fun onError(e: CometChatException) { - Log.e(TAG, "Accept failed: ${e.message}") - } - }) - } - } +## Staging or dedicated deployments - private fun rejectCall(context: Context, callData: CallData) { - CallConnectionHolder.getConnection()?.onReject() - ?: run { - CometChat.rejectCall( - callData.sessionId, - CometChatConstants.CALL_STATUS_REJECTED, - object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call rejected") - } - override fun onError(e: CometChatException) { - Log.e(TAG, "Reject failed: ${e.message}") - } - } - ) - } - } +If your Chat and Calls SDKs use `overrideClientHost`, give the Push SDK the same host. Otherwise the token is registered on the default production host, where your app id does not exist, and no push ever arrives. - companion object { - private const val TAG = "CallActionReceiver" - } -} +```kotlin +PNConfiguration.Builder(APP_ID, REGION) + .overrideClientHost("$APP_ID.apiclient-us.cometchat-staging.com/v3.0") ``` - - -```java -/** - * Handles notification button actions (Accept/Decline). - */ -public class CallActionReceiver extends BroadcastReceiver { - - private static final String TAG = "CallActionReceiver"; - - @Override - public void onReceive(Context context, Intent intent) { - CallData callData = intent.getParcelableExtra(CallNotificationManager.EXTRA_CALL_DATA); - if (callData == null) return; - - String action = intent.getAction(); - if (CallNotificationManager.ACTION_ACCEPT_CALL.equals(action)) { - acceptCall(context, callData); - } else if (CallNotificationManager.ACTION_REJECT_CALL.equals(action)) { - rejectCall(context, callData); - } - CallNotificationManager.cancelNotification(context); - } +The host is saved with the configuration, so a token that rotates while the app is closed still registers on the right host, and the Calls SDK is initialised with the matching hosts on a push wake. - private void acceptCall(Context context, CallData callData) { - CallConnection connection = CallConnectionHolder.getConnection(); - if (connection != null) { - connection.onAnswer(); - } else { - CometChat.acceptCall(callData.getSessionId(), new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Intent intent = new Intent(context, CallActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(CallActivity.EXTRA_SESSION_ID, callData.getSessionId()); - intent.putExtra(CallActivity.EXTRA_CALL_TYPE, callData.getCallType()); - context.startActivity(intent); - } - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Accept failed: " + e.getMessage()); - } - }); - } - } - - private void rejectCall(Context context, CallData callData) { - CallConnection connection = CallConnectionHolder.getConnection(); - if (connection != null) { - connection.onReject(); - } else { - CometChat.rejectCall( - callData.getSessionId(), - CometChatConstants.CALL_STATUS_REJECTED, - new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call rejected"); - } - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Reject failed: " + e.getMessage()); - } - } - ); - } - } -} -``` - - +## Tokens ---- +You do not register the push token. The SDK fetches it from Firebase and registers it on login, under the Chat auth token when there is one and the Calls auth token otherwise; it unregisters on logout. `registerToken()` / `unregisterToken()` exist for apps that set `setAutoRegisterToken(false)`. -## Step 6: Create CallActivity +Log out of the Calls SDK when the user signs out (`CometChatCalls.logout`, or `CometChatUIKit.logout` with the UI Kit). That is what stops a ring still playing for the old user and unregisters the token; `CometChat.logout` alone does neither. -The `CallActivity` hosts the actual call UI using CometChat's Calls SDK. It joins the call session and handles the call lifecycle. +## Your own incoming-call screen +Set `setIncomingCallActivity(...)`. The SDK still owns the ringtone, the pre-warm, the timeout, the keep-alive and the decline queue; only the full-screen UI is yours. ```kotlin -/** - * Activity that hosts the call UI. - * Joins the CometChat call session and displays the video/audio interface. - */ -class CallActivity : AppCompatActivity() { - - private lateinit var callSession: CallSession - private var sessionId: String? = null +class MyRingActivity : AppCompatActivity() { + private var call: IncomingCall? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_call) - - // Keep screen on during the call - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - - callSession = CallSession.getInstance() - - // Get call parameters from intent - sessionId = intent.getStringExtra(EXTRA_SESSION_ID) - val callType = intent.getStringExtra(EXTRA_CALL_TYPE) ?: "video" - - // Join the call session - sessionId?.let { joinCallSession(it, callType) } - - // Listen for session end events - setupSessionStatusListener() - } - - /** - * Joins the CometChat call session. - * This connects to the actual audio/video call. - */ - private fun joinCallSession(sessionId: String, callType: String) { - val callContainer = findViewById(R.id.callContainer) - - // Configure session settings - // See SessionSettingsBuilder for all options - val sessionSettings = CometChatCalls.SessionSettingsBuilder() - .setSessionType(if (callType == "video") SessionType.VIDEO else SessionType.AUDIO) - .build() - - // Join the session - CometChatCalls.joinSession( - sessionId, - sessionSettings, - callContainer, - object : CometChatCalls.CallbackListener() { - override fun onSuccess(p0: Void?) { - Log.d(TAG, "Joined call session successfully") - } - - override fun onError(exception: CometChatException) { - Log.e(TAG, "Failed to join call: ${exception.message}") - endCallAndFinish() - } - } - ) - } + setShowWhenLocked(true); setTurnScreenOn(true) + val call = CallBundles.getCall(intent) ?: return finish() + this.call = call + setContentView(R.layout.activity_ring) + callerName.text = call.callerName - /** - * Listens for session status changes. - * Ends the activity when the call ends. - */ - private fun setupSessionStatusListener() { - callSession.addSessionStatusListener(this, object : SessionStatusListener() { - override fun onSessionLeft() { - runOnUiThread { endCallAndFinish() } - } - - override fun onConnectionClosed() { - runOnUiThread { endCallAndFinish() } - } - }) - } + // Close when the ring ends elsewhere: caller cancelled, answered on another device, timed out. + CometChatCallNotifications.onIncomingCallScreenOpened(call.sessionId) { finish() } - /** - * Properly ends the call and finishes the activity. - */ - private fun endCallAndFinish() { - // End the Connection (updates system call state) - CallConnectionHolder.endCall() - - // Notify other participant via CometChat - sessionId?.let { sid -> - CometChat.endCall(sid, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call ended successfully") - } - override fun onError(e: CometChatException) { - Log.e(TAG, "End call error: ${e.message}") - } - }) + answerButton.setOnClickListener { + // Ask for RECORD_AUDIO (+ CAMERA if call.isVideo) yourself: answer() skips the SDK prompt. + CometChatCallNotifications.answer(this, call.sessionId) + } + declineButton.setOnClickListener { + CometChatCallNotifications.decline(this, call.sessionId) + finish() } - - finish() - } - - override fun onBackPressed() { - // Prevent accidental back press during call - // User must use the end call button } override fun onDestroy() { + call?.let { CometChatCallNotifications.onIncomingCallScreenClosed(it.sessionId) } super.onDestroy() - // Clean up if activity is destroyed while call is active - if (callSession.isSessionActive) { - callSession.leaveSession() - } - } - - companion object { - private const val TAG = "CallActivity" - const val EXTRA_SESSION_ID = "extra_session_id" - const val EXTRA_CALL_TYPE = "extra_call_type" - const val EXTRA_IS_INCOMING = "extra_is_incoming" } } ``` ```java -/** - * Activity that hosts the call UI. - */ -public class CallActivity extends AppCompatActivity { - - private static final String TAG = "CallActivity"; - public static final String EXTRA_SESSION_ID = "extra_session_id"; - public static final String EXTRA_CALL_TYPE = "extra_call_type"; - public static final String EXTRA_IS_INCOMING = "extra_is_incoming"; - - private CallSession callSession; - private String sessionId; +public class MyRingActivity extends AppCompatActivity { + private IncomingCall call; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_call); - - // Keep screen on during the call - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - - callSession = CallSession.getInstance(); - - sessionId = getIntent().getStringExtra(EXTRA_SESSION_ID); - String callType = getIntent().getStringExtra(EXTRA_CALL_TYPE); - if (callType == null) callType = "video"; - - if (sessionId != null) { - joinCallSession(sessionId, callType); - } - - setupSessionStatusListener(); - } - - private void joinCallSession(String sessionId, String callType) { - RelativeLayout callContainer = findViewById(R.id.callContainer); - - SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() - .setSessionType(callType.equals("video") ? SessionType.VIDEO : SessionType.AUDIO) - .build(); - - CometChatCalls.joinSession( - sessionId, - sessionSettings, - callContainer, - this, - new CometChatCalls.CallbackListener() { - @Override - public void onSuccess(Void unused) { - Log.d(TAG, "Joined call session successfully"); - } - - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Failed to join call: " + e.getMessage()); - endCallAndFinish(); - } - } - ); - } - - private void setupSessionStatusListener() { - callSession.addSessionStatusListener(this, new SessionStatusListener() { - @Override - public void onSessionLeft() { - runOnUiThread(() -> endCallAndFinish()); - } - - @Override - public void onConnectionClosed() { - runOnUiThread(() -> endCallAndFinish()); - } + setShowWhenLocked(true); setTurnScreenOn(true); + call = CallBundles.getCall(getIntent()); + if (call == null) { finish(); return; } + setContentView(R.layout.activity_ring); + callerName.setText(call.getCallerName()); + + // Close when the ring ends elsewhere: caller cancelled, answered on another device, timed out. + CometChatCallNotifications.onIncomingCallScreenOpened(call.getSessionId(), (Runnable) this::finish); + + answerButton.setOnClickListener(v -> { + // Ask for RECORD_AUDIO (+ CAMERA if call.isVideo()) yourself: answer() skips the SDK prompt. + CometChatCallNotifications.answer(this, call.getSessionId()); + }); + declineButton.setOnClickListener(v -> { + CometChatCallNotifications.decline(this, call.getSessionId()); + finish(); }); - } - - private void endCallAndFinish() { - CallConnectionHolder.endCall(); - - if (sessionId != null) { - CometChat.endCall(sessionId, new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call ended successfully"); - } - @Override - public void onError(CometChatException e) { - Log.e(TAG, "End call error: " + e.getMessage()); - } - }); - } - - finish(); - } - - @Override - public void onBackPressed() { - // Prevent accidental back press } @Override protected void onDestroy() { + if (call != null) CometChatCallNotifications.onIncomingCallScreenClosed(call.getSessionId()); super.onDestroy(); - if (callSession.isSessionActive()) { - callSession.leaveSession(); - } } } ``` - -```xml - - -``` - - ---- +If the same activity is also your answer activity (one screen that rings, then becomes the call), the ring ends with `RING_END_ANSWERED` a moment before the answer intent reaches it through `onNewIntent`. Use the overload that passes the reason and finish only on other reasons: -## Step 7: Configure AndroidManifest - -Add all required permissions and component declarations to your `AndroidManifest.xml`: - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +```kotlin +CometChatCallNotifications.onIncomingCallScreenOpened(call.sessionId) { reason -> + if (reason != CometChatCallNotifications.RING_END_ANSWERED) finish() +} ``` ---- - -## Step 8: Initiate Outgoing Calls +Without `onIncomingCallScreenOpened`, a cancelled call leaves your screen on the lock screen showing an Answer button for a call that no longer exists. -To make an outgoing VoIP call, use the [CometChat Chat SDK](/sdk/android/calling-overview) to initiate the call, then join the session: +## Theming the SDK's incoming-call screen - - -```kotlin -/** - * Initiates a VoIP call to another user. - * - * @param receiverId The CometChat UID of the user to call - * @param receiverName Display name of the receiver (for UI) - * @param callType "audio" or "video" - */ -fun initiateVoIPCall(receiverId: String, receiverName: String, callType: String) { - // Create a Call object with receiver info - val call = Call(receiverId, CometChatConstants.RECEIVER_TYPE_USER, callType) - - // Initiate the call via CometChat Chat SDK - // This sends a call notification to the receiver - CometChat.initiateCall(call, object : CometChat.CallbackListener() { - override fun onSuccess(call: Call) { - Log.d(TAG, "Call initiated with sessionId: ${call.sessionId}") - - // Create CallData for the outgoing call - val callData = CallData( - sessionId = call.sessionId, - callerName = receiverName, - callerUid = receiverId, - callType = callType, - callerAvatar = null - ) - - // Option 1: Use Telecom system (shows system outgoing call UI) - // CallNotificationManager.placeOutgoingCall(this@MainActivity, callData) - - // Option 2: Launch CallActivity directly (recommended) - val intent = Intent(this@MainActivity, CallActivity::class.java).apply { - putExtra(CallActivity.EXTRA_SESSION_ID, call.sessionId) - putExtra(CallActivity.EXTRA_CALL_TYPE, callType) - putExtra(CallActivity.EXTRA_IS_INCOMING, false) - } - startActivity(intent) - } +The SDK's screen and notifications read their look from resources. Override any of them by name in your app's `res/` (an app's resource wins over the library's); there is no runtime theme API. - override fun onError(e: CometChatException) { - Log.e(TAG, "Call initiation failed: ${e.message}") - Toast.makeText(this@MainActivity, "Failed to start call", Toast.LENGTH_SHORT).show() - } - }) -} +| Colour | Default | Used for | +|--------|---------|----------| +| `cometchat_pn_call_background` | `#0E0E10` | Screen background | +| `cometchat_pn_call_text` | `#F2F2F2` | Caller name | +| `cometchat_pn_call_text_secondary` | `#9A9AA1` | "Incoming audio call" line | +| `cometchat_pn_call_avatar` | `#3B5BDB` | Initials circle | +| `cometchat_pn_call_accept` | `#1B8F3A` | Answer button, notification Answer action | +| `cometchat_pn_call_decline` | `#C62828` | Decline button, notification Decline action | -// Usage example: -// initiateVoIPCall("user123", "John Doe", CometChatConstants.CALL_TYPE_VIDEO) +```xml res/values/colors.xml +#FFFFFF +#141414 +#09C26F +#F44649 ``` - - -```java -/** - * Initiates a VoIP call to another user. - */ -void initiateVoIPCall(String receiverId, String receiverName, String callType) { - // Create a Call object with receiver info - Call call = new Call(receiverId, CometChatConstants.RECEIVER_TYPE_USER, callType); - - // Initiate the call via CometChat Chat SDK - CometChat.initiateCall(call, new CometChat.CallbackListener() { - @Override - public void onSuccess(Call call) { - Log.d(TAG, "Call initiated with sessionId: " + call.getSessionId()); - - CallData callData = new CallData( - call.getSessionId(), - receiverName, - receiverId, - callType, - null - ); - - // Launch CallActivity directly - Intent intent = new Intent(MainActivity.this, CallActivity.class); - intent.putExtra(CallActivity.EXTRA_SESSION_ID, call.getSessionId()); - intent.putExtra(CallActivity.EXTRA_CALL_TYPE, callType); - intent.putExtra(CallActivity.EXTRA_IS_INCOMING, false); - startActivity(intent); - } - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Call initiation failed: " + e.getMessage()); - Toast.makeText(MainActivity.this, "Failed to start call", Toast.LENGTH_SHORT).show(); - } - }); -} -``` - - +Also overridable: ---- +- **Strings**: `cometchat_pn_incoming_call_text`, `cometchat_pn_incoming_group_call_text`, `cometchat_pn_action_answer`, `cometchat_pn_action_decline`, `cometchat_pn_call_type_audio`, `cometchat_pn_call_type_video`, `cometchat_pn_connecting_call_text`, `cometchat_pn_call_waiting_text`, `cometchat_pn_missed_call_title`, `cometchat_pn_missed_group_call_title`, `cometchat_pn_missed_call_text`, `cometchat_pn_missed_calls_summary`, and the channel names and descriptions `cometchat_pn_channel_{incoming,ongoing,missed}_{name,description}`. +- **Drawables**: `cometchat_pn_ic_call`, `cometchat_pn_ic_call_end`, `cometchat_pn_ic_notification_call`, `cometchat_pn_ic_notification_call_missed`, `cometchat_pn_bg_button_accept`, `cometchat_pn_bg_button_decline`, `cometchat_pn_bg_avatar_circle`. +- **Theme**: `CometChatPNIncomingCallTheme`. +- **Layout**: `cometchat_pn_activity_incoming_call`, which must keep the view ids `cometchat_pn_initials`, `cometchat_pn_caller_name`, `cometchat_pn_call_type`, `cometchat_pn_decline` and `cometchat_pn_answer`. -## Complete Flow Diagram +The avatar is always the caller's initials; the screen does not load the avatar URL. The ringtone is the device's default ringtone on channel `incoming_calls_v1`; because Android freezes a channel's sound at creation, the SDK never changes it afterwards. -This diagram shows the complete flow for both incoming and outgoing VoIP calls: + +If you use the CometChat UI Kit, it already overrides the six colours with its own light and dark palette. Your app's override still wins over the UI Kit's. + -```mermaid -sequenceDiagram - participant Caller - participant CallerApp - participant CometChat - participant FCM - participant ReceiverApp - participant AndroidTelecom - participant Receiver - - Note over Caller,Receiver: Outgoing Call Flow - Caller->>CallerApp: Tap call button - CallerApp->>CometChat: initiateCall() - CometChat-->>CallerApp: onSuccess(sessionId) - CallerApp->>CallerApp: Launch CallActivity - CallerApp->>CometChat: joinSession() - CometChat->>FCM: Send push notification - - Note over Caller,Receiver: Incoming Call Flow - FCM->>ReceiverApp: onMessageReceived() - ReceiverApp->>ReceiverApp: Parse call data - - alt App in Background - ReceiverApp->>AndroidTelecom: addNewIncomingCall() - AndroidTelecom->>Receiver: Show system call UI - Receiver->>AndroidTelecom: Accept/Reject - AndroidTelecom->>ReceiverApp: onAnswer()/onReject() - else App in Foreground - ReceiverApp->>Receiver: Show in-app call UI - Receiver->>ReceiverApp: Accept/Reject - end - - alt Call Accepted - ReceiverApp->>CometChat: acceptCall() - CometChat-->>ReceiverApp: onSuccess - ReceiverApp->>ReceiverApp: Launch CallActivity - ReceiverApp->>CometChat: joinSession() - CometChat-->>CallerApp: onOutgoingCallAccepted - Note over CallerApp,ReceiverApp: Call Connected - else Call Rejected - ReceiverApp->>CometChat: rejectCall() - CometChat-->>CallerApp: onOutgoingCallRejected - end +## MANUAL mode + +`setCallMode(CallMode.MANUAL)` makes the SDK parse, gate and forward only. It never rings and never touches the Calls SDK on its own. The call reaches `CometChatCalls.IncomingCallListener.onIncomingCallReceived`, and from there every step is yours: + +```kotlin +CometChatCallNotifications.Calls.showIncomingNotification(context, call) // post the ring, arm expiry +CometChatCallNotifications.Calls.verify(context, call) // pre-warm connect() +CometChatCallNotifications.Calls.keepAlive(context, call) // foreground service while ringing +CometChatCallNotifications.Calls.stopRinging(context, sessionId) +CometChatCallNotifications.Calls.showMissedCall(context, call) ``` ---- + +In MANUAL mode with nothing built, the call is silently lost: no notification, no ringtone, no screen, nothing sent to the caller, who rings out to `unanswered`. Use MANUAL only if you are rebuilding the whole ring. + -## Troubleshooting +## Knowing about a call in your own code -| Issue | Solution | -|-------|----------| -| System call UI not showing | Ensure PhoneAccount is registered and enabled. Check Settings > Apps > Phone > Calling accounts | -| Calls not received in background | Verify [FCM configuration](/notifications/android-push-notifications) and ensure high-priority notifications are enabled | -| Permission denied errors | Request `MANAGE_OWN_CALLS` permission at runtime on Android 11+ | -| Call drops immediately | Verify [CometChat authentication](/calls/android/authentication) is valid before joining session | -| Audio routing issues | Ensure `audioModeIsVoip = true` is set on the Connection | -| PhoneAccount not enabled | Call `PhoneAccountManager.openPhoneAccountSettings()` to let user enable it | +Listen on the Calls SDK, not on this SDK. It is the same callback whether the call arrived by push on a killed process or on a live one: - ```kotlin -private val requiredPermissions = arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.RECORD_AUDIO, - Manifest.permission.BLUETOOTH_CONNECT, // Android 12+ - Manifest.permission.POST_NOTIFICATIONS // Android 13+ -) +CometChatCalls.addIncomingCallListener("my-screen", object : CometChatCalls.IncomingCallListener() { + override fun onIncomingCallReceived(call: InitiatedCall) { /* badge, list refresh, analytics */ } + override fun onIncomingCallEnded(call: InitiatedCall, reason: String) { /* … */ } +}) +``` -private fun checkAndRequestPermissions() { - val permissionsToRequest = requiredPermissions.filter { - ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED - } - - if (permissionsToRequest.isNotEmpty()) { - ActivityCompat.requestPermissions( - this, - permissionsToRequest.toTypedArray(), - PERMISSION_REQUEST_CODE - ) - } -} +Ids are keys: the same id registered twice replaces the listener. Remove it when the screen goes away. -override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray -) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == PERMISSION_REQUEST_CODE) { - val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } - if (!allGranted) { - // Show explanation or disable calling features - Toast.makeText(this, "Permissions required for calling", Toast.LENGTH_LONG).show() - } - } -} +## Push SDK identifiers -companion object { - private const val PERMISSION_REQUEST_CODE = 100 -} -``` - +| Kind | Value | +|------|-------| +| Notification channels | `incoming_calls_v1` (high, ringtone, vibration), `ongoing_calls_v1` (low, "Connecting call…"), `missed_calls_v1` (default) | +| Statuses the SDK sends with `endCall` | `rejected`, `busy`, `unanswered` | +| Intent extras | `com.cometchat.pushnotification.calls.call.`; read with `CallBundles.getCall` | +| Timings | ring window 45 s default, answer connecting cap 60 s, decline queue 5 attempts | -## Related Documentation +## Troubleshooting -- [Ringing](/calls/android/ringing) - Basic in-app call signaling -- [Actions](/calls/android/actions) - Control call functionality -- [Events](/calls/android/events) - Listen for call state changes -- [Push Notifications](/notifications/android-push-notifications) - FCM setup -- [Push Integration](/notifications/push-overview) - Register push tokens with CometChat +| Symptom | Check | +|---------|-------| +| No push arrives | Package name matches the Firebase app; `google-services.json` present; provider id set; the user is logged in (the token registers on login); `overrideClientHost` matches the Chat and Calls SDKs | +| Push arrives, nothing rings | `handlePushNotification` is called for every push with `sentTimeMs`; `ensureRingReady` is `READY`; the push was not older than its ring window | +| Rings but no full-screen screen on a locked phone | `FSI_REVOKED` on Android 14+: send the user to the settings page from `settingsIntentFor` | +| Rings, then the process dies before Answer | Call pushes sent at normal priority (`BFGS denied`); send at high priority | +| Answer opens the app but joins fail with "app id does not exist" | Staging or dedicated host not given to the Push SDK with `overrideClientHost` | +| Caller never sees "Ringing…" | The caller joined right after `initiateCall` instead of listening on the prepared session | +| Ring keeps playing after the user logged out | The app logged out of the Chat SDK only; also log out of the Calls SDK | +| Tapping a missed call does nothing | Set `setMissedCallActivity` | +| Two ringtones or two screens | Legacy app-side ringing (`CallListener` overlay, own sound) still in place; remove it | + +## Related + +- [Ringing](/calls/android/ringing): starting, answering and ending calls with the Calls SDK. +- [Migrating from legacy ringing](/calls/android/migrating-from-legacy-ringing). +- [Background Handling](/calls/android/background-handling): a foreground service during the call. +- [Android Push Notifications](/notifications/android-push-notifications): chat notifications with the same SDK. diff --git a/docs.json b/docs.json index 96952f214..202e89650 100644 --- a/docs.json +++ b/docs.json @@ -1781,6 +1781,7 @@ "ui-kit/android/threaded-messages-header", "ui-kit/android/incoming-call", "ui-kit/android/outgoing-call", + "ui-kit/android/ongoing-call", "ui-kit/android/call-buttons", "ui-kit/android/call-logs", "ui-kit/android/search", @@ -5718,7 +5719,8 @@ { "group": "Migration Guide", "pages": [ - "calls/android/migration-guide-v5" + "calls/android/migration-guide-v5", + "calls/android/migrating-from-legacy-ringing" ] }, { diff --git a/notifications/android-push-notifications.mdx b/notifications/android-push-notifications.mdx index 93148c799..a3f34d759 100644 --- a/notifications/android-push-notifications.mdx +++ b/notifications/android-push-notifications.mdx @@ -1,39 +1,29 @@ --- title: "Android Push Notifications" -description: "Setup FCM and CometChat for message and call push notifications on Android." +description: "Set up FCM and the CometChat Push Notifications SDK 2.0 on Android for message notifications with inline reply and call ringing." --- - + - Reference implementation of Kotlin UI Kit, FCM and Push Notification Setup. + Reference implementations: `sample-app-kotlin-push` and `sample-app-compose-push`. ## 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. - -{/* ## What you need first - -- 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). */} +- FCM setup and the CometChat provider (credentials, Gradle, manifest). +- Initialising the Push Notifications SDK; the SDK registers and unregisters the device token itself. +- Message pushes: grouped notifications, inline reply, delivered receipts and taps, all handled by the SDK. +- Call pushes: ringing through the SDK's calls module, with the app in any state. +- App icon badge count from the `unreadMessageCount` field. ## How FCM and CometChat fit together -- **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. +- **Why FCM?** Google issues device tokens and delivers raw push payloads to Android. You add `google-services.json`, the Messaging SDK and a `FirebaseMessagingService` so the device can receive pushes. The CometChat SDK never compiles against Firebase; you own it and hand payloads in. +- **Why a CometChat provider?** The Provider ID tells CometChat which FCM credentials to use when sending to your app. The SDK registers each device token against it on login. +- **What the SDK does with a push.** `handlePushNotification` parses the payload and, for `type = chat`, posts a grouped notification with inline reply and marks the message delivered; for `type = call`, the calls module rings the device. It returns `false` for pushes that are not CometChat's, so your own handling can take over. ## 1. Prepare Firebase and CometChat @@ -55,13 +45,12 @@ From the same screen, click **Add Provider** to upload the Firebase service acco Upload FCM service account JSON -3. **App constants**: -Note down your CometChat App ID, Auth Key, and Region from the CometChat dashboard and keep them available to be added to your project's AppCrendentials.kt. -Similarly, note the FCM Provider ID you set in the CometChat dashboard and add the same in your AppConstants.kt; this will be when registering the FCM token with CometChat. +3. **App constants**: note your CometChat App ID, Auth Key and Region, and the FCM **Provider ID** you chose. The Provider ID goes into `PNConfiguration.setProviderId`; the SDK registers the device token under it. + ## 2. 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. +Two CometChat artifacts, published in lockstep. The calls one is optional: add it to make incoming calls ring. @@ -71,498 +60,191 @@ Use a version catalog and aliases (Update `applicationId`, package names, icons, 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" +cometChatUikit = "6.1.0" +cometChatPush = "2.0.0" 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" } +cometchat-uikit = { group = "com.cometchat", name = "chatuikit-kotlin-android", version.ref = "cometChatUikit" } +cometchat-push = { group = "com.cometchat", name = "push-notifications-android", version.ref = "cometChatPush" } +cometchat-push-calls = { group = "com.cometchat", name = "push-notifications-calls-android", version.ref = "cometChatPush" } 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 +```kotlin lines plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.google.services) + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("com.google.gms.google-services") } android { - compileSdk 35 - defaultConfig { - applicationId "your.package.name" - minSdk 26 - targetSdk 35 - } - kotlinOptions { jvmTarget = "11" } + defaultConfig { minSdk = 26 } } dependencies { - // CometChat - implementation(libs.cometchat.uikit) - implementation(libs.cometchat.sdk) - implementation(libs.cometchat.calls) + implementation(libs.cometchat.uikit) // or chatuikit-compose-android, or the Chat and Calls SDKs directly + implementation(libs.cometchat.push) + implementation(libs.cometchat.push.calls) // only if you want incoming calls to ring - // 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) } ``` -- 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. +- The UI Kit already depends on both push artifacts; declaring them yourself only pins the version. +- Apply the `google-services` plugin and place `google-services.json` in the same module. +- `minSdk 26`: the calls module inherits the Calls SDK's floor. The core artifact alone works from 24. +- The Chat SDK is optional for the push SDK: a calls-only app ships without it. Chat notifications and inline reply need it. ## 3. 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} +```xml lines - - - + - - - - - - - - + + - - - - - - - - ``` -- Permissions cover notifications + telecom; services/receiver wire Firebase delivery (`FCMService`), notification actions (`FCMMessageBroadcastReceiver`), and telecom UI (`CometChatVoIPConnectionService`). Point `android:name` to your `MyApplication`. - -- 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). - -## 4. 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. +Everything else the ringer needs (full-screen intent, the `phoneCall` foreground service, wake lock, vibrate, the incoming-call activity, the answer trampoline, the ring service and the inline-reply receiver) is declared by the SDK and merges in. There is no `ConnectionService`, no `MANAGE_OWN_CALLS`, `READ_PHONE_STATE` or `ANSWER_PHONE_CALLS`. -**What the core pieces do** +## 4. Initialise the SDK -- `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): +Once per install, after the UI Kit (or the Chat and Calls SDKs) is initialised. The configuration is persisted, so a push that wakes a cold process still finds it. ```kotlin lines -// In your Splash/entry activity (e.g., SplashActivity) -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - handleDeepLinking() -} - -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) +import com.cometchat.pushnotification.CometChatPushNotifications +import com.cometchat.pushnotification.PNConfiguration +import com.cometchat.uikit.kotlin.presentation.ongoingcall.ui.CometChatOngoingCallActivity +// Compose: com.cometchat.uikit.compose.presentation.ongoingcall.ui.CometChatOngoingCallActivity + +val config = PNConfiguration.Builder(APP_ID, REGION) + .setProviderId(PROVIDER_ID) // the FCM provider you created + .setNotificationSmallIcon(R.drawable.ic_notification) + .setInlineReplyEnabled(true) // default true; needs the Chat SDK + .setAnswerActivity(CometChatOngoingCallActivity::class.java) // calls: the screen opened on Answer + .apply { + // Staging or dedicated deployment: same host as the Chat and Calls SDKs + CometChatUIKit.getAuthSettings()?.overrideClientHost?.let { overrideClientHost(it) } + } + .build() - startActivity( - Intent(this, HomeActivity::class.java).apply { - putExtra(AppConstants.FCMConstants.NOTIFICATION_TYPE, notificationType) - putExtra(AppConstants.FCMConstants.NOTIFICATION_PAYLOAD, notificationPayload) - } - ) - finish() -} +CometChatPushNotifications.init(this, config) ``` -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. +| Setter | Default | What it does | +| --- | --- | --- | +| `setProviderId(id)` | none | Provider the device token is registered under. Registration fails with `PROVIDER_REQUIRED` without it | +| `setNotificationSmallIcon(res)` | none | Status-bar icon for every notification the SDK posts | +| `setInlineReplyEnabled(bool)` | `true` | Reply action on chat notifications | +| `setAnswerActivity(Class)` | none; required with the calls module | Opened when the user answers a call. UI Kit apps register `CometChatOngoingCallActivity` | +| `setIncomingCallActivity(Class?)` | the SDK's screen | Your own incoming-call screen | +| `setMissedCallActivity(Class?)` | none | Opened when a missed-call notification is tapped | +| `setDefaultRingTimeoutMs(ms)`, `setShowMissedCallNotification(bool)`, `setRequestCallPermissions(bool)` | 45 000, `true`, `true` | Ring behaviour; see [Push Ringing](/calls/android/voip-calling) | +| `overrideClientHost(host)` | none | Same value as the Chat and Calls SDKs' `overrideClientHost` | +| `setTokenRegistrar(CHAT | CALLS | AUTO)`, `setAuthToken`, `setAutoRegisterToken` | `AUTO`, none, `true` | Which login's auth token registers the device; leave the defaults | -**SplashViewModel (init UIKit + login check)** +Deep links from chat notifications: ```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) - - 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() - } +CometChatPushNotifications.setOnNotificationTapListener(object : NotificationTapListener { + override fun onNotificationTapped(context: Context, user: User?, group: Group?, message: BaseMessage?) { + // Open the conversation with `user` or `group`; `message` is the tapped message when present + context.startActivity(Intent(context, MessagesActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP + user?.let { putExtra("user", Gson().toJson(it)) } + group?.let { putExtra("group", Gson().toJson(it)) } }) } - - 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 - } - - fun checkUserIsNotLoggedIn() { - loginStatus.value = CometChatUIKit.getLoggedInUser() != null - } - - fun getLoginStatus(): LiveData = loginStatus -} +}) ``` -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)** +Suppress notifications for the chat that is on screen: ```kotlin lines -object Repository { - fun registerFCMToken(listener: CometChat.CallbackListener) { /* fetch FCM token and call registerPushToken */ } - fun unregisterFCMToken(listener: CometChat.CallbackListener) { /* call unregisterPushToken */ } - - fun rejectCallWithBusyStatus( - call: Call, - callbackListener: CometChat.CallbackListener? = null - ) { /* reject with CALL_STATUS_BUSY and notify UIKit */ } - - 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 */ } -} +// In your messages screen +override fun onResume() { super.onResume(); CometChatPushNotifications.setCurrentOpenChatId(user?.uid ?: group?.guid) } +override fun onPause() { super.onPause(); CometChatPushNotifications.setCurrentOpenChatId(null) } ``` -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)** +## 5. Hand every push to the SDK ```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 } - }) - } +class AppFCMService : FirebaseMessagingService() { - 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) {} - }) + override fun onMessageReceived(message: RemoteMessage) { + val handled = CometChatPushNotifications.handlePushNotification( + context = this, + data = message.data, + sentTimeMs = message.sentTime + ) + if (!handled) { /* not a CometChat push: your own handling */ } } - 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 + override fun onNewToken(token: String) { + CometChatPushNotifications.handleTokenRefresh(token) } } ``` -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. +- Pass `sentTimeMs`: Firebase holds pushes while a device dozes, and without it a late push rings for a call that is already over. +- Do not skip pushes while the app is in the foreground. The SDK suppresses a chat notification for the open chat itself (`setCurrentOpenChatId`), and a call push is the only signal that a call is coming in, in every app state. +- Call pushes must be sent at **high** FCM priority, or Android refuses the ringer's foreground service and the process can die between the ring and the answer. -State to set at runtime: +## 6. Tokens, login and logout -- `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. +You do not register the device token. The SDK fetches it from Firebase and registers it when the user logs in (under the Chat auth token when there is one, the Calls auth token otherwise), re-registers it when Firebase rotates it, and unregisters it on logout. -## 5. Application wiring and permissions +Log out through the UI Kit (`CometChatUIKit.logout`) or, with the SDKs directly, log out of both the Chat SDK and the Calls SDK. That is what unregisters the token and stops a call still ringing for the old user; `CometChat.logout` alone does neither. -- [`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: +Call `registerToken()` / `unregisterToken()` yourself only if you set `setAutoRegisterToken(false)`. -```kotlin lines -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - AppUtils.requestNotificationPermission(this) - configureVoIP() - handleDeepLinking() // open chats based on NOTIFICATION_TYPE/NOTIFICATION_PAYLOAD -} +### Permissions and ring readiness -private fun configureVoIP() { - CometChatVoIP.init(this, applicationInfo.loadLabel(packageManager).toString()) - launchVoIP() -} - -private fun launchVoIP() { - if (!CometChatVoIP.hasReadPhoneStatePermission(this)) { - CometChatVoIP.requestReadPhoneStatePermission(this, CometChatVoIPConstant.PermissionCode.READ_PHONE_STATE) - return - } - 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. - -## 6. Register the FCM token after login - -Call registration right after `CometChatUIKit.login()` succeeds: +Ask for `POST_NOTIFICATIONS` (Android 13+) after sign-in, then check what else could silence a notification or a ring: ```kotlin lines -FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> - if (task.isSuccessful) { - val token = task.result - CometChatNotifications.registerPushToken( - 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 */ } - } - ) +CometChatPushNotifications.ensureRingReady(this) { result -> + val readiness = result.valueOrNull() ?: return@ensureRingReady + if (readiness != RingReadiness.READY) { + // NOTIFICATIONS_OFF, CHANNEL_MUTED, FSI_REVOKED, DND_BLOCKING, MICROPHONE_DENIED + CometChatPushNotifications.settingsIntentFor(this, readiness)?.let(::startActivity) } } ``` -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. - -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 */ } - } - ) -} -``` - -Ensures a rotated FCM token is re-bound to the logged-in user; without this, pushes will stop after Firebase refreshes the token. - -## 7. Unregister the token on logout - -```kotlin lines -CometChatNotifications.unregisterPushToken(object : CometChat.CallbackListener() { - override fun onSuccess(s: String?) { /* success */ } - override fun onError(e: CometChatException) { /* handle error */ } -}) -// Then call CometChatUIKit.logout() -``` +`verifyPushSetup` and `getDiagnostics(context)` give a redacted snapshot for bug reports (whether a provider id exists, never what it is; never a token). -## 8. Badge count +## 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. -### 8.1 Enable unread badge count on the CometChat Dashboard +### 7.1 Enable unread badge count on the CometChat Dashboard 1. Go to **CometChat Dashboard → Notifications → 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. -### 8.2 Add the ShortcutBadger dependency +### 7.2 Add the ShortcutBadger dependency Add the ShortcutBadger library to your app-level `build.gradle`: @@ -572,7 +254,7 @@ dependencies { } ``` -### 8.3 Expected payload format +### 7.3 Expected payload format CometChat sends FCM data messages with this structure (relevant fields): @@ -591,14 +273,14 @@ CometChat sends FCM data messages with this structure (relevant fields): `unreadMessageCount` is a string representing the total unread messages across all conversations for the logged-in user. -### 8.4 Update the app badge from the push payload +### 7.4 Update the app badge from the push payload -Inside your notification service (for example `FCMService.onMessageReceived`), parse `unreadMessageCount` and update the badge: +The Push SDK posts the notification; the badge stays yours. In `onMessageReceived`, read `unreadMessageCount` from the same payload you hand to the SDK: ```kotlin lines import me.leolin.shortcutbadger.ShortcutBadger -// Inside onMessageReceived, after receiving the message: +// Inside onMessageReceived, next to handlePushNotification: val unreadCountStr: String? = message.data["unreadMessageCount"] unreadCountStr?.toIntOrNull()?.let { count -> if (count >= 0) { @@ -611,20 +293,7 @@ unreadCountStr?.toIntOrNull()?.let { count -> `ShortcutBadger` uses launcher-specific APIs (Samsung, Huawei, Xiaomi, etc.) to display a badge number on the app icon. Passing `0` clears the badge. -### 8.5 Show unread count in the notification - -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") -``` - -- `setNumber(count)` displays a count badge on the notification icon. -- `setSubText()` shows the unread count below the notification title. - -### 8.6 Clear badge when the app opens +### 7.5 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: @@ -635,98 +304,50 @@ override fun onResume() { } ``` -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 -} -``` - -Use `message` for deep links (`CometChatHelper.processMessage`) and `type/callAction` to branch chat vs call flows. */} - -## 9. 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. +This keeps the badge in sync with the actual unread state. -## 10. Handle call pushes (ConnectionService) +## 8. Message pushes -- 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. +Handled by the SDK: one grouped notification per conversation with the sender's name and avatar, an inline **Reply** action that sends the reply through the Chat SDK and refreshes the notification, a delivered receipt sent for the message, and a tap that calls your `NotificationTapListener`. Nothing to build; customise the small icon and the reply action through `PNConfiguration`. -## 11. Customize notification text or parse payloads +To change the body text of a push before it is sent, set `pushNotification` in the message's metadata: -Parse the push into a `BaseMessage` for deep links: - -```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 -} +```kotlin lines +val meta = JSONObject().put("pushNotification", "Custom notification body") +customMessage.metadata = meta +CometChat.sendCustomMessage(customMessage, object : CometChat.CallbackListener() { /* … */ }) ``` -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. +## 9. Call pushes -Override the push body before sending: +Handled by the calls module: it decides whether to ring, shows a call-style notification with Answer and Decline and a full-screen screen over the lock screen, plays the ringtone, pre-warms the call session, asks for the microphone and camera on Answer, opens your answer activity, sends declines and timeouts through a durable queue, posts missed-call notifications and handles a call during a call. UI Kit apps register `CometChatOngoingCallActivity` as the answer activity and are done. -```kotlin -val meta = JSONObject().put("pushNotification", "Custom notification body") -customMessage.metadata = meta -CometChat.sendCustomMessage(customMessage, object : CallbackListener() {}) -``` +Everything about it, including your own ring screen, theming and what happens when the app is killed, is on [Push Ringing](/calls/android/voip-calling). Starting and ending calls is on [Ringing](/calls/android/ringing). -Adds a `pushNotification` field in metadata so CometChat uses your custom text as the push body for that message. -## 12. 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. -## 13. Testing checklist +## 10. 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). +1. Install on a physical device and grant notifications (Android 13+ needs `POST_NOTIFICATIONS`). +2. Log in and confirm the token registered (`getDiagnostics` → `hasProviderId`, `verifyPushSetup` → `OK` or `TEST_PUSH_UNAVAILABLE`). 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. + - Foreground: a grouped notification shows unless that chat is open. + - Background or killed: the tap opens the right 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. +5. Call from another user: + - The call-style notification and, on a locked phone, the full-screen screen appear with the caller's name. + - Answer opens the call screen and joins; Decline ends the ring and the caller sees "declined". + - Let it ring out: a missed-call notification appears and the caller sees "unanswered". + - Kill the app and call again: it still rings. +6. Log out and confirm no further pushes arrive; log in as another user and confirm theirs do. ## 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 | Package name matches the Firebase app, `google-services.json` present, notification permission granted, Provider ID correct and Push Notifications enabled in the dashboard, user logged in (the token registers on login) | +| No pushes on a staging or dedicated deployment | `overrideClientHost` on `PNConfiguration` matches the Chat and Calls SDKs | +| Pushes stop after a while | `onNewToken` not forwarded to `handleTokenRefresh` | +| Chat notification tap does nothing | `setOnNotificationTapListener` not set, or the activity's launch mode drops extras | +| Calls do not ring | `push-notifications-calls-android` missing, `setAnswerActivity` not set, push sent at normal priority, or `ensureRingReady` not `READY` | +| Ringing continues after logout | Logged out of the Chat SDK only; log out of the Calls SDK too (`CometChatUIKit.logout`) | +| Inline reply crashes | Do not strip FCM or `RemoteInput` classes in ProGuard/R8 | +| Badge count not showing | **Unread Badge Count** enabled in the dashboard, ShortcutBadger added, launcher supports badges (Samsung, Huawei, Xiaomi) | diff --git a/sdk/android/v5/calling-overview.mdx b/sdk/android/v5/calling-overview.mdx index 698eb563c..fae822120 100644 --- a/sdk/android/v5/calling-overview.mdx +++ b/sdk/android/v5/calling-overview.mdx @@ -1,10 +1,16 @@ --- title: "Calling" sidebarTitle: "Calling" -description: "Voice and video calling integration with CometChat for Android" +description: "Voice and video calling for Android: the Calls SDK owns calls and ringing, the Push Notifications SDK rings the device, and the Chat SDK's calling methods are deprecated." --- -CometChat's Calling SDK enables you to add voice and video calling to your Android application. The Calling SDK works alongside the Chat SDK to provide a complete communication experience including 1-on-1 calls, group calls, and conference sessions. +Voice and video calling on Android is provided by the CometChat Calls SDK, with incoming calls rung by the Push Notifications SDK. The Chat SDK is used for chat; it no longer takes part in calling. + + +**The Chat SDK's calling APIs are deprecated.** `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall`, `getCallParticipantCount`, `addCallListener` / `removeCallListener`, `CallListener`, `OngoingCallListener` and `CallSettings` still compile but are replaced by the Calls SDK: `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `joinSession`, `CallSession.leave()` and `addIncomingCallListener`. Ringing the callee is done by the Push Notifications SDK calls module. See [Ringing](/calls/android/ringing) and [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). + +Not deprecated: the `Call` message model and its getters, `CATEGORY_CALL`, the `CALL_STATUS_*` and `CALL_TYPE_*` constants, because call messages in a conversation's history still use them. + ## Key Features @@ -12,7 +18,7 @@ CometChat's Calling SDK enables you to add voice and video calling to your Andro - **Screen Sharing** — Share your screen during calls for collaboration - **Call Recording** — Record calls and access recordings later - **Call Logs** — Track call history with detailed metadata -- **Ringing & Notifications** — Built-in call ringing flow with accept/reject +- **Ringing & Notifications** — Incoming calls ring through the Push Notifications SDK, with the app in any state - **Customizable UI** — Presenter mode and video view customization ## Choose Your Version @@ -27,5 +33,5 @@ CometChat's Calling SDK enables you to add voice and video calling to your Andro -If you are using CometChat UI Kits, calling is already integrated. The Calling SDK is only needed for custom calling implementations. +If you are using CometChat UI Kits, calling is already integrated; see [Calling Integration](/ui-kit/android/calling-integration). The Calls SDK is only needed for custom calling implementations. diff --git a/sdk/android/v5/real-time-listeners.mdx b/sdk/android/v5/real-time-listeners.mdx index 2a1561c9b..c0e38d41d 100644 --- a/sdk/android/v5/real-time-listeners.mdx +++ b/sdk/android/v5/real-time-listeners.mdx @@ -490,6 +490,10 @@ The `AIAssistantListener` delivers real-time streaming events. After a run compl ## Call Listener + +**Deprecated.** `CallListener` and `addCallListener()` are deprecated together with the Chat SDK's other calling methods. Calls now ring through the Push Notifications SDK and announce themselves on the Calls SDK's `CometChatCalls.addIncomingCallListener` (`onIncomingCallReceived`, `onIncomingCallEnded(call, reason)`), whether the call arrived by push on a killed process or while the app was running. The caller's "answered" event is `ParticipantEventListener.onParticipantJoined` on the prepared session. See [Ringing](/calls/android/ringing) and [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). The section below is kept for apps still on the legacy flow. + + The `CallListener` class provides you with live events related to calls. Below are the callback methods provided by the `CallListener` class. | Method | Information | diff --git a/ui-kit/android/call-buttons.mdx b/ui-kit/android/call-buttons.mdx index 0ba0619fe..dd7638f1d 100644 --- a/ui-kit/android/call-buttons.mdx +++ b/ui-kit/android/call-buttons.mdx @@ -5,6 +5,8 @@ description: "Voice and video call buttons that initiate calls for a given user `CometChatCallButtons` renders voice and video call buttons and initiates calls for the bound `User` or `Group`. Place it in a `CometChatMessageHeader` or anywhere a call action is needed. +For a user, a tap calls `CometChatCalls.initiateCall` through the Calls SDK (the backend rings the callee by push) and opens the [Outgoing Call](/ui-kit/android/outgoing-call) screen. For a group, it sends a meeting message to the group and opens the [Ongoing Call](/ui-kit/android/ongoing-call) screen as a conference; members join by tapping the message. The Chat SDK's calling APIs are not used. + --- ## Where It Fits @@ -160,9 +162,39 @@ CometChatCallButtons(
+#### `onCallInitiated` + +Fires when a 1:1 call was created, with the `Call`. Default: opens the Outgoing Call screen. Provide the callback to host the outgoing screen yourself. + + + + +```kotlin lines +callButtons.setOnCallInitiated { call -> + // Open your own outgoing screen with call.sessionId +} +``` + + + + +```kotlin lines +CometChatCallButtons( + user = user, + onCallInitiated = { call -> /* host the outgoing screen yourself */ } +) +``` + + + + +#### `onStartDirectCall` + +Fires when a group call message was sent, with the `BaseMessage`. Default: opens the conference screen. + #### `onError` -Fires on internal errors (network failure, auth issue, SDK exception). +Fires when a call could not be started: another call is active (`ACTIVE_CALL`), the Calls SDK refused, or the network failed. @@ -192,10 +224,17 @@ CometChatCallButtons( | Event | Fires when | Payload | | --- | --- | --- | -| `ccOutgoingCall` | An outgoing call is initiated | `Call` | -| `ccCallAccepted` | A call is accepted by the recipient | `Call` | -| `ccCallRejected` | A call is rejected by the recipient | `Call` | -| `ccCallEnded` | A call is ended | `Call` | +| `ccOutgoingCall` | An outgoing 1:1 call was created | `Call` | +| `ccCallRejected` | The outgoing screen closed without an answer (End tapped, declined, busy, unanswered, cancelled) | `Call` | +| `ccCallEnded` | A 1:1 call ended on this device | `Call` | + +### SDK Events (Real-Time, Automatic) + +| Calls SDK listener | Internal behavior | +| --- | --- | +| `IncomingCallListener.onIncomingCallEnded` | Re-enables the buttons after a call ends anywhere | + +A second call is refused with `ACTIVE_CALL` while a Calls SDK session is prepared or joined, while the UI Kit tracks an active call, or while a meeting is active. --- @@ -207,6 +246,8 @@ CometChatCallButtons( | `setGroup(group)` | `group = group` | Set the group to call (required for group calls) | | `setOnVoiceCallClick { }` | `onVoiceCallClick = { }` | Override voice call button behavior | | `setOnVideoCallClick { }` | `onVideoCallClick = { }` | Override video call button behavior | +| `setOnCallInitiated { }` | `onCallInitiated = { }` | Override what happens once a 1:1 call is created | +| `setOnStartDirectCall { }` | `onStartDirectCall = { }` | Override what happens once a group call message is sent | | `setOnError { }` | `onError = { }` | Error callback | | `setVoiceCallButtonVisibility(View.GONE)` | `hideVoiceCallButton = true` | Toggle voice call button | | `setVideoCallButtonVisibility(View.GONE)` | `hideVideoCallButton = true` | Toggle video call button | @@ -292,11 +333,11 @@ See [ViewModel & Data](/ui-kit/android/customization-viewmodel-data) for state o View call history - - Incoming call notification with accept/reject - - Outgoing call screen with end-call button + Calling and ringing states before the answer + + + The in-call screen Display user/group info in the toolbar diff --git a/ui-kit/android/call-features.mdx b/ui-kit/android/call-features.mdx index af17a5d73..91e027c30 100644 --- a/ui-kit/android/call-features.mdx +++ b/ui-kit/android/call-features.mdx @@ -7,149 +7,65 @@ description: "Add one-on-one and group audio/video calling to your Android app u | Field | Value | | --- | --- | -| Kotlin (XML Views) | `com.cometchat:chatuikit-kotlin-android` + `com.cometchat:calls-sdk-android` | -| Jetpack Compose | `com.cometchat:chatuikit-compose-android` + `com.cometchat:calls-sdk-android` | -| Required setup | `CometChatUIKit.init()` then `CometChatUIKit.login()` — Calls SDK must also be installed | -| Call features | Incoming Call, Outgoing Call, Call Logs, Call Buttons, Ongoing Call | -| Key components | `CometChatCallButtons`, `CometChatIncomingCall`, `CometChatOutgoingCall`, `CometChatCallLogs`, `CometChatOngoingCall` | -| Auto-detection | UI Kit automatically detects the Calls SDK and enables call UI components | -| Related | [Getting Started](/ui-kit/android/getting-started), [Core Features](/ui-kit/android/core-features), [Call Log Details Guide](/ui-kit/android/guide-call-log-details) | +| Kotlin (XML Views) | `com.cometchat:chatuikit-kotlin-android` (brings `calls-sdk-android`, `push-notifications-android`, `push-notifications-calls-android`) | +| Jetpack Compose | `com.cometchat:chatuikit-compose-android` (same transitive SDKs) | +| Required setup | `CometChatUIKit.init()` with `setEnableCalling(true)`, `CometChatUIKit.login()`, `CometChatPushNotifications.init()` with `setAnswerActivity(CometChatOngoingCallActivity::class.java)`, a `FirebaseMessagingService` that calls `handlePushNotification` | +| Incoming calls | Rung by the Push Notifications SDK (notification, full-screen screen, ringtone), in every app state. No call listener, no incoming-call component | +| Call features | Call Buttons, Outgoing Call, Ongoing Call, Call Logs, Call Log Details | +| Key components | `CometChatCallButtons`, `CometChatOutgoingCall`, `CometChatOngoingCall` / `CometChatOngoingCallActivity`, `CometChatCallLogs` | +| Deprecated | `CometChatIncomingCall` (kept for a custom ring screen), Chat SDK `CometChat.addCallListener` / `initiateCall` / `acceptCall` / `rejectCall` / `endCall` | +| Related | [Calling Integration](/ui-kit/android/calling-integration), [Push Ringing](/calls/android/voip-calling), [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing), [Call Log Details Guide](/ui-kit/android/guide-call-log-details) | -CometChat's Calls feature allows you to seamlessly integrate one-on-one as well as group audio and video calling capabilities into your application. This document provides a technical overview of these features, as implemented in the Android UI Kit. - -## Integration - -First, make sure that you've correctly integrated the UI Kit library into your project. If you haven't done this yet or are facing difficulties, refer to our [Getting Started](/ui-kit/android/getting-started) guide. - -Once you've successfully integrated the UI Kit, the next step is to add the CometChat Calls SDK to your project. This is necessary to enable the calling features in the UI Kit. - -### Step 1: Add Calls SDK Dependency +CometChat's Calls feature lets you add one-on-one and group audio and video calling to your application. This page is an overview of how calling works in the Android UI Kit and which component handles each part. + +## How a call works + +```mermaid +sequenceDiagram + participant A as Caller · UI Kit + participant BE as CometChat backend + participant P as Callee · Push SDK + participant U as Callee · UI Kit + + A->>BE: Call button → CometChatCalls.initiateCall + Note over A: Outgoing call screen: "calling…" + BE-->>P: Push (initiated) + P->>P: Rings: notification, full-screen screen, ringtone + P-->>A: Callee's device reached → "ringing…" + P->>U: Answer → CometChatOngoingCallActivity + U->>U: joinSession + U-->>A: Callee joined → caller joins + Note over A,U: In call. Hang-up leaves the session on both sides. +``` -Add the following dependency to your `build.gradle.kts` file: +- **Placing a call** is done by the UI Kit through the Calls SDK. The Chat SDK is not involved. +- **Ringing the callee** is done by the Push Notifications SDK from the call push. It rings with the app in the foreground, in the background or not running, draws the incoming-call screen, asks for the microphone and camera on Answer, and opens the UI Kit's call screen. +- **Decline, timeout, busy and missed calls** are handled by the Push SDK. The caller's outgoing screen closes with the reason. +- **The call itself** runs in the Calls SDK session hosted by `CometChatOngoingCallActivity`; hanging up leaves the session. - - -```kotlin build.gradle.kts -dependencies { - implementation("com.cometchat:chatuikit-kotlin-android:6.0.6") - implementation("com.cometchat:calls-sdk-android:5.0.1") -} -``` - - - -```kotlin build.gradle.kts -dependencies { - implementation("com.cometchat:chatuikit-compose-android:6.0.6") - implementation("com.cometchat:calls-sdk-android:5.0.1") -} -``` - - +## Integration -After adding this dependency, sync your project. The Android UI Kit will automatically detect the Calls SDK and activate the calling features. +Three steps, all on the [Calling Integration](/ui-kit/android/calling-integration) page: -### Step 2: Verify Call Buttons Appear +1. `setEnableCalling(true)` in `UIKitSettings`. The UI Kit initialises the Calls SDK and logs the user into it. +2. `CometChatPushNotifications.init()` with `setProviderId` and `setAnswerActivity(CometChatOngoingCallActivity::class.java)`, plus a three-line `FirebaseMessagingService`. +3. `POST_NOTIFICATIONS`, `RECORD_AUDIO` and `CAMERA` in the manifest, and `ensureRingReady` after sign-in. -Once the Calls SDK is integrated, you will see the `CometChatCallButtons` component automatically rendered in the [MessageHeader](/ui-kit/android/message-header) component. This provides users with quick access to initiate audio and video calls. +Once calling is active, the call buttons render in the [MessageHeader](/ui-kit/android/message-header): -### Step 3: Add Call Listener for Incoming Calls - -To receive incoming calls globally in your app, you will need to add a `CallListener`. This should be added before you initialize the CometChat UI Kit. We recommend creating a custom Application class and adding the call listener there. - -When an incoming call is received, you can display the `CometChatIncomingCall` component using the current activity context. - - - -```kotlin -class BaseApplication : Application() { - - companion object { - private val LISTENER_ID = "${BaseApplication::class.java.simpleName}${System.currentTimeMillis()}" - } - - override fun onCreate() { - super.onCreate() - - CometChat.addCallListener(LISTENER_ID, object : CometChat.CallListener { - override fun onIncomingCallReceived(call: Call) { - // Get the current activity context - val currentActivity = getCurrentActivity() // Implement this method - - currentActivity?.let { - // Create and display the incoming call component - val incomingCallView = CometChatIncomingCall(it) - incomingCallView.call = call - incomingCallView.fitsSystemWindows = true - incomingCallView.onError = OnError { exception -> - // Handle errors - } - - // Display the component (e.g., as dialog or snackbar) - } - } - - override fun onOutgoingCallAccepted(call: Call) { - // Handle outgoing call acceptance - } - - override fun onOutgoingCallRejected(call: Call) { - // Handle outgoing call rejection - } - - override fun onIncomingCallCancelled(call: Call) { - // Handle incoming call cancellation - } - }) - } -} -``` - - - -```kotlin -class BaseApplication : Application() { - - companion object { - private val LISTENER_ID = "${BaseApplication::class.java.simpleName}${System.currentTimeMillis()}" - } - - override fun onCreate() { - super.onCreate() - - CometChat.addCallListener(LISTENER_ID, object : CometChat.CallListener { - override fun onIncomingCallReceived(call: Call) { - CometChatCallActivity.launchIncomingCallScreen(this@BaseApplication, call, null) - // Pass null or IncomingCallConfiguration if need to configure CometChatIncomingCall component - } - - override fun onOutgoingCallAccepted(call: Call) { - // Handle outgoing call acceptance - } - - override fun onOutgoingCallRejected(call: Call) { - // Handle outgoing call rejection - } - - override fun onIncomingCallCancelled(call: Call) { - // Handle incoming call cancellation - } - }) - } -} -``` - - + +There is no call listener to add. Apps that registered `CometChat.addCallListener` to show `CometChatIncomingCall` should remove it; the Chat SDK's calling APIs are deprecated. See [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). + ## Call Components -The CometChat Android UI Kit provides five main components for implementing calling features in your app. Each component handles a specific part of the calling experience. +The CometChat Android UI Kit provides these components for calling. Each handles a specific part of the experience. ### Call Buttons @@ -163,17 +79,17 @@ The `CometChatCallButtons` component provides users with quick access to initiat ### Incoming Call -The `CometChatIncomingCall` component displays when a user receives an incoming call. It provides a full-screen interface showing caller information and call controls. +Incoming calls are rung and drawn by the Push Notifications SDK, so there is no UI Kit component to place. Its screen is themed by the `cometchat_pn_call_*` colour resources, which the UI Kit already sets to its palette. The deprecated `CometChatIncomingCall` component remains for apps that register their own ring screen. -[Learn more about Incoming Call →](/ui-kit/android/incoming-call) +[Incoming Call (deprecated) →](/ui-kit/android/incoming-call) ### Outgoing Call -The `CometChatOutgoingCall` component manages the outgoing call experience. It displays while waiting for the recipient to answer and automatically transitions to the active call screen once accepted. +The `CometChatOutgoingCall` component manages the outgoing call experience. It shows "calling…", then "ringing…" once the callee's device is reached, closes with the reason if the call is declined, busy or unanswered, and transitions to the active call screen once the callee joins. @@ -193,7 +109,9 @@ The `CometChatCallLogs` component displays a history of all call activities, inc ### Ongoing Call -The `CometChatOngoingCall` component renders the active call screen with video feeds, mute/unmute controls, camera toggle, and end-call actions. +The `CometChatOngoingCall` component renders the active call screen with video feeds, mute/unmute controls, camera toggle, and end-call actions. `CometChatOngoingCallActivity` hosts it, supports picture-in-picture, and is the activity the Push SDK opens when the user answers an incoming call. + +[Learn more about Ongoing Call →](/ui-kit/android/ongoing-call) ### Call Log Details diff --git a/ui-kit/android/call-logs.mdx b/ui-kit/android/call-logs.mdx index 34d6381d8..d8b685280 100644 --- a/ui-kit/android/call-logs.mdx +++ b/ui-kit/android/call-logs.mdx @@ -3,7 +3,7 @@ title: "Call Logs" description: "Scrollable list of call logs for the logged-in user with caller names, avatars, call status, and timestamps." --- -`CometChatCallLogs` renders a scrollable list of call logs for the logged-in user with caller names, avatars, call status indicators, and timestamps. +`CometChatCallLogs` renders a scrollable list of call logs for the logged-in user with caller names, avatars, call status indicators, and timestamps. The call icon on a row calls that user back through the Calls SDK and opens the [Outgoing Call](/ui-kit/android/outgoing-call) screen. The list refreshes itself when a call starts or ends. @@ -26,14 +26,16 @@ check**, so simply placing it on a screen is fatal unless both conditions hold. Both are required: ```kotlin -// app/build.gradle.kts -implementation("com.cometchat:calls-sdk-android:5.0.+") +// UIKitSettings — this is what makes the UI Kit init CometChatCalls +UIKitSettings.UIKitSettingsBuilder().setEnableCalling(true) ``` ```json -// app/src/main/assets/cometchat-settings.json — this is what makes the UI Kit init CometChatCalls +// or app/src/main/assets/cometchat-settings.json { "uiKit": { "enableCalling": true } } ``` +The Calls SDK ships with the UI Kit; the `NoClassDefFoundError` row applies only to builds that exclude it. + The component **compiles fine** in every case — the failure is runtime-only, so a build cannot warn you. The same applies to the other calling components. @@ -273,9 +275,34 @@ CometChatCallLogs( +#### `onCallIconClick` + +Fires when the call icon on a row is tapped. Default: calls the other participant back with the log's call type (`audio` or `video`) and opens the Outgoing Call screen. Provide the callback to replace that. + + + + +```kotlin lines +callLogs.setOnCallTypeIconClick { callLog -> + // Custom call-back logic +} +``` + + + + +```kotlin lines +CometChatCallLogs( + onCallIconClick = { callLog -> /* custom call-back logic */ } +) +``` + + + + #### `onError` -Fires on internal errors (network failure, auth issue, SDK exception). +Fires when the list could not be loaded, and when a call-back from the list could not be placed (another call active, the Calls SDK refused). A failed call-back does not replace the list with an error state. @@ -362,6 +389,15 @@ CometChatCallLogs( | `setToolbarVisibility(View.GONE)` | `hideToolbar = true` | Toggle toolbar | | `setSeparatorVisibility(View.GONE)` | `hideSeparator = true` | Toggle list separators | | `setTitle("Call History")` | `title = "Call History"` | Custom toolbar title | +| `setOnCallTypeIconClick { }` | `onCallIconClick = { }` | Override the call-back on the row's call icon | + +### Call status + +Rows are classified with `CallLogsUtils`: a call is **missed** when it was incoming and its status is `unanswered`, `missed` or `cancelled`; **video** when its type is `video` or `audio/video`. Statuses come from `UIKitConstants.CallStatusConstants`; the Calls SDK's `CometChatCallsConstants` is deprecated. + +### Live updates + +The list registers a Calls SDK `IncomingCallListener` and reloads when a call for the logged-in user starts or ends, on either side, and again when the connection is restored. No Chat SDK listener is involved. --- @@ -802,6 +838,8 @@ CometChatCallLogs( +`CometChatCallLogsViewModel` also exposes `initiateCall(callLog)` / `initiateCall(callLog, callType)` to call back from your own row UI, `initiatedCall` (the `Call` to open an outgoing screen with) and `callInitiationError`. + See [ViewModel & Data](/ui-kit/android/customization-viewmodel-data) for ListOperations, state observation, and custom repositories. --- diff --git a/ui-kit/android/calling-integration.mdx b/ui-kit/android/calling-integration.mdx index ede933702..7c39001a6 100644 --- a/ui-kit/android/calling-integration.mdx +++ b/ui-kit/android/calling-integration.mdx @@ -1,43 +1,47 @@ --- title: "Calling Integration" -description: "Add voice and video calling to your Android UI Kit application using chatuikit-kotlin or chatuikit-compose." +description: "Add voice and video calling to your Android UI Kit application using chatuikit-kotlin or chatuikit-compose: Calls SDK, push ringing, permissions and logout." --- ## Overview -This guide walks you through adding voice and video calling capabilities to your Android application using the CometChat UI Kit. +With calling enabled, the UI Kit places calls from the message header and call logs, shows the outgoing call screen, and hosts the in-call screen. Incoming calls ring through the CometChat Push Notifications SDK: it shows the call notification and the full-screen incoming-call screen, plays the ringtone, and opens the UI Kit's call screen when the user answers, with the app in the foreground, in the background or not running at all. Make sure you've completed the [Getting Started](/ui-kit/android/getting-started) guide before proceeding. - - -**`enableCalling: true` makes the Calls SDK dependency mandatory — otherwise the app crashes at -launch.** With the flag set, `CometChatUIKit.initFromSettings()` calls `initCometChatCalls()`, which -throws if `com.cometchat:calls-sdk-android` is absent: - -``` -java.lang.NoClassDefFoundError: Failed resolution of: - Lcom/cometchat/calls/core/CometChatCalls$SessionSettingsBuilder; - at com.cometchat.uikit.core.CometChatUIKit.initCometChatCalls(CometChatUIKit.kt:229) +```mermaid +sequenceDiagram + participant A as Caller · UI Kit + participant BE as CometChat backend + participant P as Callee · Push SDK + participant U as Callee · UI Kit + + A->>BE: Call button → CometChatCalls.initiateCall + Note over A: Outgoing call screen: "calling…" + BE-->>P: Push (initiated) + P->>P: Rings: notification, full-screen screen, ringtone + P-->>A: Callee's device reached → "ringing…" + P->>U: Answer → mic/camera prompt → CometChatOngoingCallActivity + U->>U: joinSession + U-->>A: Callee joined → caller joins + Note over A,U: In call. Hang-up leaves the session on both sides. ``` -The crash happens in `onCreate`, **before any UI renders** — the flag name gives no hint that a -dependency is implied. Change the flag and the dependency together, or leave `enableCalling` as -`false` in an app that does not use calling. - +What you write: the UI Kit settings with calling enabled, the Push SDK configuration, a three-line Firebase service, and the manifest permissions. There is no call listener to register and no incoming-call UI to show. -## Add the Calls SDK +## Step 1: Dependencies -Add the CometChat Calls SDK dependency alongside your chosen UI Kit module: +The UI Kit core module depends on the Calls SDK and on both Push Notifications SDK artifacts, so your app gets them transitively. Add the UI Kit module and Firebase Messaging: ```kotlin build.gradle.kts dependencies { - implementation("com.cometchat:chatuikit-kotlin-android:6.0.6") - implementation("com.cometchat:calls-sdk-android:5.0.1") + implementation("com.cometchat:chatuikit-kotlin-android:6.1.0") + implementation(platform("com.google.firebase:firebase-bom:33.7.0")) + implementation("com.google.firebase:firebase-messaging") } ``` @@ -45,14 +49,185 @@ dependencies { ```kotlin build.gradle.kts dependencies { - implementation("com.cometchat:chatuikit-compose-android:6.0.6") - implementation("com.cometchat:calls-sdk-android:5.0.1") + implementation("com.cometchat:chatuikit-compose-android:6.1.0") + implementation(platform("com.google.firebase:firebase-bom:33.7.0")) + implementation("com.google.firebase:firebase-messaging") } ``` -After adding this dependency, the Android UI Kit will automatically detect it and activate the calling features. +Apply the `google-services` plugin and put `google-services.json` in the app module. `minSdk` must be 26 or higher: the Calls SDK's React Native runtime declares it. + + +To pin the SDK versions yourself, declare them explicitly; the UI Kit requires `calls-sdk-android` 5.1 or newer and `push-notifications-android` / `push-notifications-calls-android` 2.0 or newer. + + +## Step 2: Enable calling in the UI Kit + +Set `setEnableCalling(true)` in `UIKitSettings`. `CometChatUIKit.init` then initialises the Calls SDK with the same app id, region and hosts, and `CometChatUIKit.login` logs the user into it. Do not call `CometChatCalls.init` yourself: the Calls SDK keeps the hosts of its first initialisation. + + + +```kotlin +val settings = UIKitSettings.UIKitSettingsBuilder() + .setAppId(APP_ID) + .setRegion(REGION) + .setAuthKey(AUTH_KEY) + .subscribePresenceForAllUsers() + .setEnableCalling(true) + .build() + +CometChatUIKit.init(this, settings, object : CometChat.CallbackListener() { + override fun onSuccess(result: String) { initPushNotifications() } + override fun onError(e: CometChatException) { Log.e(TAG, "init failed: ${e.message}") } +}) +``` + + +```kotlin +val settings = UIKitSettings.UIKitSettingsBuilder() + .setAppId(APP_ID) + .setRegion(REGION) + .setAuthKey(AUTH_KEY) + .subscribePresenceForAllUsers() + .setEnableCalling(true) + .build() + +CometChatUIKit.init(this, settings, object : CometChat.CallbackListener() { + override fun onSuccess(result: String) { initPushNotifications() } + override fun onError(e: CometChatException) { Log.e(TAG, "init failed: ${e.message}") } +}) +``` + + + +Run this in `Application.onCreate` from stored credentials, not only after a screen opens. A push can start the app process to ring a call, and the answer must find a ready SDK. + + +**Enabling calling in an app that already has logged-in users requires a re-login.** `CometChatUIKit.login()` short-circuits when the same uid already has a persisted session, so the Calls SDK never logs in: the Calls SDK has no auth token and `CometChatCallLogs` shows a silent error state (logcat: `User auth token cannot be null`). After calling is first enabled, log out and log in again (`CometChatUIKit.logout(...)` then `CometChatUIKit.login(...)`), and wire `onError` on `CometChatCallLogs` during development so this failure is visible. + + +### Staging or dedicated deployments + +Pass all three hosts; the UI Kit forwards them to the Chat SDK and the Calls SDK. The Push SDK gets the client host in the next step. + +```kotlin +UIKitSettings.UIKitSettingsBuilder() + .setAppId(APP_ID).setRegion(REGION).setAuthKey(AUTH_KEY) + .overrideAdminHost("$APP_ID.api-$REGION.example.com/v3") + .overrideClientHost("$APP_ID.apiclient-$REGION.example.com/v3") + .overrideCallsHost("$APP_ID.call-$REGION.example.com/v3") + .setEnableCalling(true) + .build() +``` + +## Step 3: Configure push ringing + +Initialise the Push Notifications SDK once, after the UI Kit (the FCM provider id and the answer activity are all it needs). `CometChatOngoingCallActivity` is the UI Kit's in-call screen; registered as the answer activity it reads the answered call from its intent and joins. + + + +```kotlin +import com.cometchat.uikit.kotlin.presentation.ongoingcall.ui.CometChatOngoingCallActivity + +private fun initPushNotifications() { + val config = PNConfiguration.Builder(APP_ID, REGION) + .setProviderId(PROVIDER_ID) // FCM provider id from the dashboard + .setAnswerActivity(CometChatOngoingCallActivity::class.java) // the UI Kit's call screen + .setNotificationSmallIcon(R.drawable.ic_notification) + .apply { + // Same host as the Chat and Calls SDKs, or the token registers on the wrong app + CometChatUIKit.getAuthSettings()?.overrideClientHost?.let { overrideClientHost(it) } + } + .build() + CometChatPushNotifications.init(this, config) +} +``` + + +```kotlin +import com.cometchat.uikit.compose.presentation.ongoingcall.ui.CometChatOngoingCallActivity + +private fun initPushNotifications() { + val config = PNConfiguration.Builder(APP_ID, REGION) + .setProviderId(PROVIDER_ID) // FCM provider id from the dashboard + .setAnswerActivity(CometChatOngoingCallActivity::class.java) // the UI Kit's call screen + .setNotificationSmallIcon(R.drawable.ic_notification) + .apply { + // Same host as the Chat and Calls SDKs, or the token registers on the wrong app + CometChatUIKit.getAuthSettings()?.overrideClientHost?.let { overrideClientHost(it) } + } + .build() + CometChatPushNotifications.init(this, config) +} +``` + + + +Then hand every push to the SDK from your Firebase service: + +```kotlin +class AppFCMService : FirebaseMessagingService() { + override fun onMessageReceived(message: RemoteMessage) { + CometChatPushNotifications.handlePushNotification(this, message.data, message.sentTime) + } + override fun onNewToken(token: String) { + CometChatPushNotifications.handleTokenRefresh(token) + } +} +``` + +```xml AndroidManifest.xml + + + + + +``` + +The push token is registered on login and unregistered on logout by the SDK. Do not skip call pushes when the app is in the foreground: the push is the only incoming-call signal, in every app state. Call pushes must be sent at high FCM priority. + +Every option of `PNConfiguration` (ring timeout, missed-call notifications, your own ring screen, MANUAL mode) is on the Calls SDK's [Push Ringing](/calls/android/voip-calling) page. + +## Step 4: Permissions + +```xml AndroidManifest.xml + + + +``` + +The remaining permissions and components (full-screen intent, the ringing foreground service, the ring screen) come from the Push SDK's manifest. + +At runtime: + +- **Notifications**: ask for `POST_NOTIFICATIONS` (Android 13+) after sign-in, then call `CometChatPushNotifications.ensureRingReady(activity)` and show the user anything other than `READY`. Notifications off, a muted call channel, a revoked full-screen intent and Do Not Disturb all silence calls with no error. +- **Microphone and camera**: the Push SDK asks when the user answers, and the UI Kit's call screen asks once more when it opens; a denied microphone joins muted. Ask at sign-in as well if you do not want the first call to be where the user meets the prompt. + +```kotlin +CometChatPushNotifications.ensureRingReady(this) { result -> + val readiness = result.valueOrNull() ?: return@ensureRingReady + if (readiness != RingReadiness.READY) { + Toast.makeText(this, "Incoming calls may not ring: $readiness", Toast.LENGTH_LONG).show() + } +} +``` + +Run `ensureRingReady` only after your own permission request has returned; Android drops a request made while another dialog is open. + +## Step 5: Log out through the UI Kit + +```kotlin +CometChatUIKit.logout(object : CometChat.CallbackListener() { + override fun onSuccess(s: String) { /* go to login */ } + override fun onError(e: CometChatException) { } +}) +``` + +`CometChatUIKit.logout` ends a call in progress, cancels an outgoing call still ringing, then logs out of the Chat SDK and the Calls SDK; the Push SDK stops a ring still playing for the old user and unregisters the token. `CometChat.logout` alone does none of the calling clean-up. + +## What the UI Kit does from here Once calling is active, the voice and video call buttons render in the [MessageHeader](/ui-kit/android/message-header): @@ -74,91 +249,50 @@ CometChatMessageHeader(
- -**Enabling calling in an app that already has logged-in users requires a re-login.** `CometChatUIKit.login()` short-circuits when the same uid already has a persisted session — it returns `onSuccess` without logging the Calls SDK in, so the Calls SDK has no auth token and `CometChatCallLogs` shows a silent error state (logcat: `User auth token cannot be null`). After calling is first enabled, log out and log in again (`CometChatUIKit.logout(...)` then `CometChatUIKit.login(...)`), and wire `onError` on `CometChatCallLogs` during development so this failure is visible. - - -## Set Up Call Listener +| Moment | What happens | Component | +|--------|--------------|-----------| +| Tap a call button | `CometChatCalls.initiateCall`, then the outgoing screen opens | [Call Buttons](/ui-kit/android/call-buttons) | +| Waiting | "calling…", then "ringing…" when the callee's device is reached; End cancels the call; 45 s with no answer ends it as unanswered | [Outgoing Call](/ui-kit/android/outgoing-call) | +| Callee declines / is busy / does not answer | The outgoing screen closes with the reason | Outgoing Call | +| Incoming call | The Push SDK rings and draws the incoming-call screen | Push SDK (no UI Kit component) | +| Answer | `CometChatOngoingCallActivity` opens and joins the session | [Ongoing Call](/ui-kit/android/ongoing-call) | +| Hang-up | The session is left; the other side's screen closes | Ongoing Call | +| Call history | Call logs list from the Calls SDK; the call icon calls back | [Call Logs](/ui-kit/android/call-logs) | +| Group call | The group call button sends a meeting message; tapping it joins the conference | Call Buttons, Ongoing Call | -To receive incoming calls globally in your app, add a `CallListener` before initializing the CometChat UI Kit. We recommend creating a custom Application class: +To react to calls in your own code (a badge, analytics, refreshing a screen), listen on the Calls SDK with `CometChatCalls.addIncomingCallListener`, the same callback the UI Kit uses internally. Do not use the Chat SDK's deprecated `CallListener`. - - -```kotlin -class BaseApplication : Application() { - companion object { - private val LISTENER_ID = "${BaseApplication::class.java.simpleName}${System.currentTimeMillis()}" - } +## Placing a call from your own screen - override fun onCreate() { - super.onCreate() - CometChat.addCallListener(LISTENER_ID, object : CometChat.CallListener { - override fun onIncomingCallReceived(call: Call) { - // Get the current activity context - val currentActivity = getCurrentActivity() // Implement this method - - currentActivity?.let { - val incomingCallView = CometChatIncomingCall(it) - incomingCallView.call = call - incomingCallView.fitsSystemWindows = true - incomingCallView.onError = OnError { exception -> - // Handle errors - } - - // Display the component (e.g., as dialog or full-screen overlay) - } - } - - override fun onOutgoingCallAccepted(call: Call) { - // Handle accepted outgoing call - } - - override fun onOutgoingCallRejected(call: Call) { - // Handle rejected outgoing call - } - - override fun onIncomingCallCancelled(call: Call) { - // Handle cancelled incoming call - } - }) - } +```kotlin +lifecycleScope.launch { + CallInitiationUtils.initiateUserCall(user.uid, CometChatConstants.CALL_TYPE_AUDIO, user) + .onSuccess { call -> CometChatCallActivity.launchOutgoingCallScreen(context, call, null) } + .onFailure { e -> Log.e(TAG, "Could not start the call: ${e.message}") } } ``` - - -```kotlin -class BaseApplication : Application() { - companion object { - private val LISTENER_ID = "${BaseApplication::class.java.simpleName}${System.currentTimeMillis()}" - } +`CometChatCallActivity` is `com.cometchat.uikit.kotlin.calls.CometChatCallActivity` for XML Views and `com.cometchat.uikit.compose.calls.CometChatCallActivity` for Compose. A call is refused with `ACTIVE_CALL` while another session is active. - override fun onCreate() { - super.onCreate() - CometChat.addCallListener(LISTENER_ID, object : CometChat.CallListener { - override fun onIncomingCallReceived(call: Call) { - CometChatCallActivity.launchIncomingCallScreen(this@BaseApplication, call, null) - // Pass null or IncomingCallConfiguration if need to configure CometChatIncomingCall component - } - - override fun onOutgoingCallAccepted(call: Call) { - // Handle accepted outgoing call - } - - override fun onOutgoingCallRejected(call: Call) { - // Handle rejected outgoing call - } - - override fun onIncomingCallCancelled(call: Call) { - // Handle cancelled incoming call - } - }) - } -} +## Styling the incoming-call screen + +The incoming-call screen belongs to the Push SDK and is themed by resources, not by the UI Kit theme. The UI Kit ships overrides for the six colour resources so the screen matches its light and dark palette; your app's own override wins: + +```xml res/values/colors.xml +#FFFFFF +#141414 +#727272 +#6852D6 +#09C26F +#F44649 ``` - - + +Strings, drawables, the theme and the layout can be overridden the same way; the full list is under [Theming the SDK's incoming-call screen](/calls/android/voip-calling#theming-the-sdks-incoming-call-screen). To draw the screen yourself, register the UI Kit's `CometChatCallActivity` or your own activity with `setIncomingCallActivity`; see the deprecated [Incoming Call](/ui-kit/android/incoming-call) component. + +## Upgrading from an app that used `CometChat.addCallListener` + +Remove the Application-level `CallListener`, the incoming-call overlay or `launchIncomingCallScreen` call, any ringtone you played, the busy check, and the Push SDK 1.0 `setVoIPEnabled` / `setOnCallAnsweredHandler` wiring, then follow the steps above. The method-by-method mapping is on [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). diff --git a/ui-kit/android/color-resources.mdx b/ui-kit/android/color-resources.mdx index a3a1f2c70..aa542a7a2 100644 --- a/ui-kit/android/color-resources.mdx +++ b/ui-kit/android/color-resources.mdx @@ -217,3 +217,20 @@ Background, stroke, text, and icon colors are derived from the neutral scale by | `iconTintHighlight` | `primary` (#6852D6) | `primary` (#6852D6) | This means overriding a neutral color automatically updates all tokens that reference it. + +--- + +## Incoming Call Screen Colors + +The incoming-call screen is drawn by the Push Notifications SDK, not by the UI Kit theme. It reads six colour resources, which the UI Kit overrides with its palette in light and dark mode. Override them again in your app's `res/values/` and `res/values-night/` to change the screen; your app's values win. + +| Resource | UI Kit light | UI Kit dark | Used for | +| --- | --- | --- | --- | +| `cometchat_pn_call_background` | #FFFFFF | #141414 | Screen background | +| `cometchat_pn_call_text` | #141414 | #FFFFFF | Caller name | +| `cometchat_pn_call_text_secondary` | #727272 | #989898 | Call type line | +| `cometchat_pn_call_avatar` | #6852D6 | #6852D6 | Initials circle | +| `cometchat_pn_call_accept` | #09C26F | #0B9F5D | Answer button and notification action | +| `cometchat_pn_call_decline` | #F44649 | #C73C3E | Decline button and notification action | + +These are resources, so a theme set at runtime on the UI Kit does not reach the ring screen. Strings, drawables and the layout can be overridden the same way; see [Theming the SDK's incoming-call screen](/calls/android/voip-calling#theming-the-sdks-incoming-call-screen). diff --git a/ui-kit/android/getting-started-jetpack.mdx b/ui-kit/android/getting-started-jetpack.mdx index 5a4377e55..206104d3b 100644 --- a/ui-kit/android/getting-started-jetpack.mdx +++ b/ui-kit/android/getting-started-jetpack.mdx @@ -14,7 +14,7 @@ description: "Step-by-step guide to integrate the CometChat Jetpack Compose UI K | Login | `CometChatUIKit.login("UID", callback)` — must resolve before rendering components | | Order | `init()` → `login()` → render. Breaking this order = blank screen | | Auth Key | Dev/testing only. Use Auth Token in production | -| Calling | Optional. Add `com.cometchat:calls-sdk-android` to enable voice/video | +| Calling | Optional. `setEnableCalling(true)` in `UIKitSettings` + push ringing setup; the Calls and Push SDKs ship with the UI Kit | | Min SDK | Android 9.0 (API 28) | @@ -47,21 +47,9 @@ Auth Key is for development only. In production, generate Auth Tokens server-sid --- - -**`enableCalling: true` makes the Calls SDK dependency mandatory — otherwise the app crashes at -launch.** With the flag set, `CometChatUIKit.initFromSettings()` calls `initCometChatCalls()`, which -throws if `com.cometchat:calls-sdk-android` is absent: - -``` -java.lang.NoClassDefFoundError: Failed resolution of: - Lcom/cometchat/calls/core/CometChatCalls$SessionSettingsBuilder; - at com.cometchat.uikit.core.CometChatUIKit.initCometChatCalls(CometChatUIKit.kt:229) -``` - -The crash happens in `onCreate`, **before any UI renders** — the flag name gives no hint that a -dependency is implied. Change the flag and the dependency together, or leave `enableCalling` as -`false` in an app that does not use calling. - + +**Calling.** Set `enableCalling` to `true` to turn on voice and video calling; the UI Kit then initialises the Calls SDK and logs the user into it. The Calls SDK and the Push Notifications SDK ship with the UI Kit, so there is no extra dependency, but incoming calls only ring once push ringing is configured. See [Calling Integration](/ui-kit/android/calling-integration). Leave the flag `false` in an app that does not use calling. + ## Step 1 — Create an Android Project @@ -116,10 +104,9 @@ android { dependencies { // CometChat Jetpack Compose UI Kit - implementation("com.cometchat:chatuikit-compose-android:6.0.6") - - // (Optional) Voice/video calling - implementation("com.cometchat:calls-sdk-android:5.0.1") + implementation("com.cometchat:chatuikit-compose-android:6.1.0") + // Voice/video calling needs nothing more here: the Calls SDK and the Push Notifications SDK + // come with the UI Kit. See Calling Integration for push ringing (Firebase Messaging). } ``` diff --git a/ui-kit/android/getting-started-kotlin.mdx b/ui-kit/android/getting-started-kotlin.mdx index a60485102..422b7641c 100644 --- a/ui-kit/android/getting-started-kotlin.mdx +++ b/ui-kit/android/getting-started-kotlin.mdx @@ -14,7 +14,7 @@ description: "Step-by-step guide to integrate the CometChat Kotlin UI Kit with X | Login | `CometChatUIKit.login("UID", callback)` — must resolve before rendering components | | Order | `init()` → `login()` → render. Breaking this order = blank screen | | Auth Key | Dev/testing only. Use Auth Token in production | -| Calling | Optional. Add `com.cometchat:calls-sdk-android` to enable voice/video | +| Calling | Optional. `setEnableCalling(true)` in `UIKitSettings` + push ringing setup; the Calls and Push SDKs ship with the UI Kit | | Min SDK | Android 9.0 (API 28) | @@ -47,21 +47,9 @@ Auth Key is for development only. In production, generate Auth Tokens server-sid --- - -**`enableCalling: true` makes the Calls SDK dependency mandatory — otherwise the app crashes at -launch.** With the flag set, `CometChatUIKit.initFromSettings()` calls `initCometChatCalls()`, which -throws if `com.cometchat:calls-sdk-android` is absent: - -``` -java.lang.NoClassDefFoundError: Failed resolution of: - Lcom/cometchat/calls/core/CometChatCalls$SessionSettingsBuilder; - at com.cometchat.uikit.core.CometChatUIKit.initCometChatCalls(CometChatUIKit.kt:229) -``` - -The crash happens in `onCreate`, **before any UI renders** — the flag name gives no hint that a -dependency is implied. Change the flag and the dependency together, or leave `enableCalling` as -`false` in an app that does not use calling. - + +**Calling.** Set `enableCalling` to `true` to turn on voice and video calling; the UI Kit then initialises the Calls SDK and logs the user into it. The Calls SDK and the Push Notifications SDK ship with the UI Kit, so there is no extra dependency, but incoming calls only ring once push ringing is configured. See [Calling Integration](/ui-kit/android/calling-integration). Leave the flag `false` in an app that does not use calling. + ## Step 1 — Create an Android Project @@ -111,10 +99,9 @@ android { dependencies { // CometChat Kotlin UI Kit - implementation("com.cometchat:chatuikit-kotlin-android:6.0.6") - - // (Optional) Voice/video calling - implementation("com.cometchat:calls-sdk-android:5.0.1") + implementation("com.cometchat:chatuikit-kotlin-android:6.1.0") + // Voice/video calling needs nothing more here: the Calls SDK and the Push Notifications SDK + // come with the UI Kit. See Calling Integration for push ringing (Firebase Messaging). } ``` diff --git a/ui-kit/android/incoming-call.mdx b/ui-kit/android/incoming-call.mdx index ac6ddb5bf..d03e37183 100644 --- a/ui-kit/android/incoming-call.mdx +++ b/ui-kit/android/incoming-call.mdx @@ -1,9 +1,13 @@ --- title: "Incoming Call" -description: "Full-screen incoming call banner with caller avatar, name, call type indicator, and accept/reject buttons." +description: "Deprecated incoming call screen, kept for apps that draw the ring screen themselves; incoming calls now ring through the Push Notifications SDK." --- -`CometChatIncomingCall` renders when the logged-in user receives an incoming voice or video call, displaying the caller's information and providing accept/reject controls. + +**Deprecated.** Incoming calls now ring through the CometChat Push Notifications SDK, which draws its own incoming-call screen in every app state, including when the app is not running. `CometChatIncomingCall` is kept only for apps that opt into drawing the ring screen themselves with `PNConfiguration.setIncomingCallActivity`. New integrations need no incoming-call component: see [Calling Integration](/ui-kit/android/calling-integration). + + +`CometChatIncomingCall` shows the caller's avatar, name and call type with accept and decline controls. In the current ringing flow the Push SDK owns the ringtone, the ring timeout, the pre-warmed session and the decline; this component only draws the screen and forwards Accept and Decline to the Push SDK. @@ -11,62 +15,78 @@ description: "Full-screen incoming call banner with caller avatar, name, call ty --- -## Where It Fits +## When to use it + +| You want | Do this | +| --- | --- | +| The default incoming-call screen | Nothing. The Push SDK's screen is themed by the `cometchat_pn_call_*` colour resources; the UI Kit already matches them to its palette. See [Styling the incoming-call screen](/ui-kit/android/calling-integration#styling-the-incoming-call-screen) | +| The UI Kit's incoming-call screen instead of the Push SDK's | Register the UI Kit's `CometChatCallActivity` as the ring screen (below). You get this component's view slots and style | +| A fully custom ring screen | Your own activity with the Push SDK's `CometChatCallNotifications` API; see [Your own incoming-call screen](/calls/android/voip-calling#your-own-incoming-call-screen) | + +--- + +## Quick Start: the UI Kit screen as the ring screen -`CometChatIncomingCall` is a call-handling component. It renders the incoming call screen and transitions to the ongoing call screen when the user accepts. Wire it to `CometChatOngoingCallActivity` after the user accepts the call. +Register `CometChatCallActivity` with the Push SDK. It reads the ringing call from the intent, shows over the lock screen, hosts `CometChatIncomingCall`, asks for the microphone and camera on Accept, answers and declines through the Push SDK, and closes itself when the call ends elsewhere. -```xml activity_call.xml lines - -``` - ```kotlin lines -val incomingCall = findViewById(R.id.incoming_call) -incomingCall.setCall(call) +import com.cometchat.uikit.kotlin.calls.CometChatCallActivity +import com.cometchat.uikit.kotlin.presentation.ongoingcall.ui.CometChatOngoingCallActivity + +PNConfiguration.Builder(APP_ID, REGION) + .setProviderId(PROVIDER_ID) + .setAnswerActivity(CometChatOngoingCallActivity::class.java) + .setIncomingCallActivity(CometChatCallActivity::class.java) + .build() ``` ```kotlin lines -CometChatIncomingCall( - call = call -) +import com.cometchat.uikit.compose.calls.CometChatCallActivity +import com.cometchat.uikit.compose.presentation.ongoingcall.ui.CometChatOngoingCallActivity + +PNConfiguration.Builder(APP_ID, REGION) + .setProviderId(PROVIDER_ID) + .setAnswerActivity(CometChatOngoingCallActivity::class.java) + .setIncomingCallActivity(CometChatCallActivity::class.java) + .build() ``` ---- +### Embedding the component yourself -## Quick Start +If you host the component in your own ring activity, that activity must also tell the Push SDK when it opens and closes, so the SDK can dismiss it when the caller cancels or the ring times out. `CallRingingUtils` wraps the Push SDK calls and converts the push's call into the `Call` the component takes. -Add to your layout XML: +```kotlin lines +class MyRingActivity : AppCompatActivity() { + private var sessionId: String? = null -```xml lines - -``` + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setShowWhenLocked(true); setTurnScreenOn(true) + setContentView(R.layout.activity_ring) -Set the `Call` object — this is required: + val call = CallRingingUtils.getCall(intent) ?: return finish() + sessionId = call.sessionId + CallRingingUtils.onRingScreenOpened(call.sessionId) { finish() } -```kotlin lines -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.layout_activity) + findViewById(R.id.incoming_call).setCall(call) + } - val incomingCall = findViewById(R.id.incoming_call) - incomingCall.setCall(call) + override fun onDestroy() { + sessionId?.let { CallRingingUtils.onRingScreenClosed(it) } + super.onDestroy() + } } ``` @@ -74,18 +94,30 @@ override fun onCreate(savedInstanceState: Bundle?) { ```kotlin lines -@Composable -fun IncomingCallScreen() { - CometChatIncomingCall( - call = call - ) +class MyRingActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setShowWhenLocked(true); setTurnScreenOn(true) + val call = CallRingingUtils.getCall(intent) ?: return finish() + CallRingingUtils.onRingScreenOpened(call.sessionId) { finish() } + setContent { + CometChatTheme { + CometChatIncomingCall(call = call, onRejectClick = { finish() }) + } + } + } + + override fun onDestroy() { + CallRingingUtils.getCall(intent)?.let { CallRingingUtils.onRingScreenClosed(it.sessionId) } + super.onDestroy() + } } ``` -Prerequisites: CometChat SDK initialized with `CometChatUIKit.init()`, a user logged in, and the UI Kit dependency added. +Prerequisites: `CometChatUIKit.init()` with `setEnableCalling(true)`, the Push SDK initialised with `setAnswerActivity(CometChatOngoingCallActivity::class.java)`, and a user logged in. --- @@ -95,7 +127,7 @@ Prerequisites: CometChat SDK initialized with `CometChatUIKit.init()`, a user lo #### `onAcceptClick` -Fires when the user taps the accept button. +Fires when the user taps Accept. By default the component answers through the Push SDK (`CometChatCallNotifications.answer`), which stops the ring and opens the answer activity (`CometChatOngoingCallActivity`), and the ring screen is closed by the Push SDK. Provide the callback to replace that behaviour. Ask for the microphone (and camera) before answering: the Push SDK only prompts for its notification's Answer action. @@ -112,7 +144,9 @@ incomingCall.setOnAcceptClickListener { ```kotlin lines CometChatIncomingCall( call = call, - onAcceptClick = { /* custom accept logic */ } + onAcceptClick = { /* custom accept logic */ }, + // Runs before the default answer; call proceed() once permissions are granted + onAcceptRequested = { proceed -> requestMicAndCamera { proceed() } } ) ``` @@ -121,7 +155,7 @@ CometChatIncomingCall( #### `onRejectClick` -Fires when the user taps the reject button. +Fires when the user taps Decline. By default the component declines through the Push SDK (`CometChatCallNotifications.decline`), which sends `rejected`, or `busy` when the user is in another call, through a queue that survives the process dying. Provide the callback to replace that behaviour. @@ -178,15 +212,16 @@ CometChatIncomingCall( | Event | Fires when | Payload | | --- | --- | --- | -| `ccCallAccepted` | A call is accepted | `Call` | -| `ccCallRejected` | A call is rejected | `Call` | +| `ccCallAccepted` | Accept tapped on this screen | `Call` | +| `ccCallRejected` | Decline tapped on this screen | `Call` | ### SDK Events (Real-Time, Automatic) -| SDK Listener | Internal behavior | +| Calls SDK listener | Internal behavior | | --- | --- | -| `onIncomingCallReceived` | Triggers the incoming call screen display | -| `onIncomingCallCancelled` | Dismisses the incoming call screen | +| `IncomingCallListener.onIncomingCallEnded` for this session | The ring ended elsewhere (caller cancelled, answered on another device, timed out): `rejectedCall` is emitted so the screen closes | + +The component does not open itself on an incoming call. The Push SDK launches the registered ring activity; the Chat SDK's `CallListener` is deprecated and not used. --- @@ -198,8 +233,9 @@ CometChatIncomingCall( | `setOnAcceptClickListener { }` | `onAcceptClick = { }` | Override accept button behavior | | `setOnRejectClickListener { }` | `onRejectClick = { }` | Override reject button behavior | | `setOnError { }` | `onError = { }` | Error callback | -| `setDisableSoundForCalls(true)` | `disableSoundForCalls = true` | Disable incoming call ringtone | -| `setCustomSoundForCalls(R.raw.tone)` | `customSoundForCalls = R.raw.tone` | Custom ringtone | +| — | `onAcceptRequested = { proceed -> }` | Compose only: gate the answer (e.g. a permission prompt), then call `proceed()` | +| `setDisableSoundForCalls(true)` | `disableSoundForCalls = true` | No effect when the Push SDK is present: the Push SDK plays the ringtone and the component stays silent to avoid two ringtones | +| `setCustomSoundForCalls(R.raw.tone)` | `customSoundForCalls = R.raw.tone` | Only used without the Push SDK calls module | --- @@ -400,6 +436,8 @@ CometChatIncomingCall( +`CometChatIncomingCallStyle` is deprecated with the component; it only styles this screen. The Push SDK's own screen is themed by the `cometchat_pn_call_*` resources instead. + See [Component Styling](/ui-kit/android/component-styling) for the full reference. --- diff --git a/ui-kit/android/llms-android-v6.mdx b/ui-kit/android/llms-android-v6.mdx index b7b1e4190..2b57a3dd3 100644 --- a/ui-kit/android/llms-android-v6.mdx +++ b/ui-kit/android/llms-android-v6.mdx @@ -99,9 +99,10 @@ attributes, long-tail components, theming tokens, or feature enablement. ## Components — calling - [Call Buttons](/ui-kit/android/call-buttons) -- [Incoming Call](/ui-kit/android/incoming-call) - [Outgoing Call](/ui-kit/android/outgoing-call) +- [Ongoing Call](/ui-kit/android/ongoing-call) — the in-call screen; also the Push SDK answer activity - [Call Logs](/ui-kit/android/call-logs) +- [Incoming Call](/ui-kit/android/incoming-call) — deprecated; incoming calls ring through the Push Notifications SDK, no component needed ## Components — search, AI & notifications - [Search](/ui-kit/android/search) @@ -134,7 +135,8 @@ the symptom, not the component. | App crashes on the chat screen: `The style on this component requires your app theme to be Theme.MaterialComponents` | [Getting started (Kotlin)](/ui-kit/android/getting-started-kotlin) — inherit `CometChatTheme.DayNight` | | Toolbar renders **under the status bar** / title collides with the clock | [Conversation + message view](/ui-kit/android/conversation-message-view) · [One-to-one chat](/ui-kit/android/one-to-one-chat) — `enableEdgeToEdge()` needs insets CONSUMED | | Composer crashes / `ActivityResultLauncher not initialized` | [Troubleshooting](/ui-kit/android/troubleshooting) — build the composer in `onCreate` | -| Calling crashes: `NoClassDefFoundError: CometChatCalls…` or `Please call the CometChatCalls.init()` | [Calling integration](/ui-kit/android/calling-integration) — the artifact AND `enableCalling` are a pair | +| Calling crashes: `Please call the CometChatCalls.init()` | [Calling integration](/ui-kit/android/calling-integration) — `setEnableCalling(true)` is missing | +| Incoming calls never ring | [Calling integration](/ui-kit/android/calling-integration) — `CometChatPushNotifications.init` with `setAnswerActivity(CometChatOngoingCallActivity::class.java)` + a `FirebaseMessagingService` that calls `handlePushNotification`; the Chat SDK `CallListener` is deprecated and fires nothing | | Components render but stay empty | init/login did not resolve — [Getting started (Kotlin)](/ui-kit/android/getting-started-kotlin) / [(Compose)](/ui-kit/android/getting-started-jetpack) | | Keyboard covers the composer | `windowSoftInputMode="adjustResize"` — [One-to-one chat](/ui-kit/android/one-to-one-chat) | | Colours ignored / theme not applied | [Theme introduction](/ui-kit/android/theme-introduction) | diff --git a/ui-kit/android/methods.mdx b/ui-kit/android/methods.mdx index e4e53101e..f3acb689b 100644 --- a/ui-kit/android/methods.mdx +++ b/ui-kit/android/methods.mdx @@ -74,6 +74,9 @@ The `UIKitSettings` is an important parameter of the `init()` function. It serve | **setAIFeatures** | `List` | Sets the AI Features that need to be added in UI Kit | | **setExtensions** | `List` | Sets the list of extension that need to be added in UI Kit | | **dateTimeFormatterCallback** | `DateTimeFormatterCallback` | Interface containing callback methods to format different types of timestamps. | +| **setEnableCalling** | `Boolean` | Turns on calling: the UI Kit initialises the Calls SDK during `init()` and logs the user into it during `login()`. Default `false`. See [Calling Integration](/ui-kit/android/calling-integration) | +| **overrideAdminHost / overrideClientHost / overrideCallsHost** | `String` | Hosts of a staging or dedicated deployment, forwarded to the Chat SDK and the Calls SDK. Give the Push Notifications SDK the same client host | +| **setCallSettingsBuilder** | `CometChatCalls.SessionSettingsBuilder` | Default session settings for the call screens | | **setEnableThreadSubscription** | `Boolean` | *Deprecated in v6.1.0.* Global switch for the thread subscription feature. Default `true` from v6.1.0 (`false` on v6.0.x, where it is the opt-in). Kept as the only app-wide opt-out — pass `false` to turn the feature off everywhere. To hide a single surface, prefer the per-screen visibility methods. See [Thread Subscription](/ui-kit/android/guide-threaded-messages#thread-subscription) | **Usage:** @@ -192,7 +195,11 @@ CometChatUIKit.logout(object : CometChat.CallbackListener() { }) ``` -> **What this does:** Logs out the current user, clears the session data, and tears down the internal event system. Call this before logging in a different user to avoid conflicts. +> **What this does:** Ends a call in progress and cancels an outgoing call still ringing, logs out of the Chat SDK and then the Calls SDK, clears the session data and tears down the internal event system. On the Calls logout the Push Notifications SDK stops a ring still playing for the old user and unregisters the push token. Call this before logging in a different user to avoid conflicts. + + +Use `CometChatUIKit.logout`, not `CometChat.logout`. The Chat SDK's logout alone leaves the Calls SDK signed in: a ringing call is not stopped and later declines go out with the old user's token. + --- diff --git a/ui-kit/android/ongoing-call.mdx b/ui-kit/android/ongoing-call.mdx new file mode 100644 index 000000000..dedd0927c --- /dev/null +++ b/ui-kit/android/ongoing-call.mdx @@ -0,0 +1,328 @@ +--- +title: "Ongoing Call" +description: "In-call screen that joins the call session and renders the Calls SDK's call UI; also the activity the Push SDK opens when the user answers." +--- + +`CometChatOngoingCall` renders the active call: the Calls SDK's video tiles and controls (mute, camera, switch camera, end). `CometChatOngoingCallActivity` hosts it full-screen, supports picture-in-picture, and is the **answer activity** for push ringing: when the user answers an incoming call, the Push SDK opens it with the call in its intent and it joins the session. + +--- + +## Where It Fits + +Every call ends up here: + +| Path | How the screen opens | +|------|----------------------| +| Incoming call answered | The Push SDK starts `CometChatOngoingCallActivity` (registered with `PNConfiguration.setAnswerActivity`). It reads the answered call from the intent and joins | +| Outgoing call answered | The [Outgoing Call](/ui-kit/android/outgoing-call) screen hands over once the callee joins: XML Views embed `CometChatOngoingCall` in the same activity, Compose launches `CometChatOngoingCallActivity` | +| Group call / meeting | [Call Buttons](/ui-kit/android/call-buttons) send a meeting message; tapping it launches the activity with `CallWorkFlow.MEETING` | +| Your own flow | `CometChatOngoingCallActivity.launchOngoingCallActivity(context, sessionId, callType)` or the component directly | + + + + +```kotlin lines +import com.cometchat.uikit.kotlin.presentation.ongoingcall.ui.CometChatOngoingCallActivity + +CometChatOngoingCallActivity.launchOngoingCallActivity( + context = this, + sessionId = sessionId, + callType = CometChatConstants.CALL_TYPE_VIDEO +) +``` + + + + +```kotlin lines +import com.cometchat.uikit.compose.presentation.ongoingcall.ui.CometChatOngoingCallActivity + +CometChatOngoingCallActivity.launchOngoingCallActivity( + context = context, + sessionId = sessionId, + callType = CometChatConstants.CALL_TYPE_VIDEO +) +``` + + + + +--- + +## Quick Start + +### As the answer activity (incoming calls) + +Register it once in the Push SDK configuration. No other code: the activity reads the session id and call type from the intent, asks for the microphone and camera, and calls `joinSession`. + +```kotlin lines +PNConfiguration.Builder(APP_ID, REGION) + .setProviderId(PROVIDER_ID) + .setAnswerActivity(CometChatOngoingCallActivity::class.java) + .build() +``` + +See [Calling Integration](/ui-kit/android/calling-integration) for the full setup. + +### Launching it yourself + + + + +```kotlin lines +CometChatOngoingCallActivity.launchOngoingCallActivity( + context = this, + sessionId = sessionId, + callType = CometChatConstants.CALL_TYPE_AUDIO, + callWorkFlow = CallWorkFlow.DEFAULT, // MEETING for a group call + callSettingsBuilder = null, // or your SessionSettingsBuilder + style = null +) +``` + + + + +```kotlin lines +CometChatOngoingCallActivity.launchOngoingCallActivity( + context = context, + sessionId = sessionId, + callType = CometChatConstants.CALL_TYPE_AUDIO, + callWorkFlow = CallWorkFlow.DEFAULT, // MEETING for a group call + callSettingsBuilder = null, // or your SessionSettingsBuilder + style = null +) +``` + + + + +### Embedding the component + +Use the component directly when you host the call in your own screen. + + + + +```xml activity_call.xml lines + +``` + +```kotlin lines +val ongoingCall = findViewById(R.id.ongoing_call) +ongoingCall.setSessionId(sessionId) +ongoingCall.setCallType(CometChatConstants.CALL_TYPE_VIDEO) +ongoingCall.setCallWorkFlow(CallWorkFlow.DEFAULT) +ongoingCall.setSessionSettingsBuilder(CometChatCalls.SessionSettingsBuilder()) +ongoingCall.startCall() +``` + + + + +```kotlin lines +CometChatOngoingCall( + sessionId = sessionId, + callType = CometChatConstants.CALL_TYPE_VIDEO, + callWorkFlow = CallWorkFlow.DEFAULT, + onCallEnded = { finish() }, + onError = { e -> Log.e("Call", e.message ?: "") } +) +``` + + + + +Prerequisites: `CometChatUIKit.init()` with `setEnableCalling(true)`, a user logged in, and `RECORD_AUDIO` and `CAMERA` declared in the manifest. + +--- + +## Actions and Events + +### Callback Methods + +#### `onCallEnded` + +Fires when this device has left the session: the user tapped End, the other side hung up, the session timed out, or the connection closed. Close the screen here. + + + + +The hosting `CometChatOngoingCallActivity` finishes itself. When you embed the component, observe the ViewModel's events: + +```kotlin lines +lifecycleScope.launch { + ongoingCall.getViewModel()?.events?.collect { event -> + if (event is OngoingCallEvent.CallEnded) finish() + } +} +``` + + + + +```kotlin lines +CometChatOngoingCall( + sessionId = sessionId, + callType = callType, + onCallEnded = { finish() } +) +``` + + + + +#### `onError` + +Fires when the session could not be joined (bad session id, no network, the Calls SDK not logged in) or the microphone permission is missing. + + + + +```kotlin lines +ongoingCall.setOnError { exception -> + Log.e("OngoingCall", "Error: ${exception.message}") +} +``` + + + + +```kotlin lines +CometChatOngoingCall( + sessionId = sessionId, + callType = callType, + onError = { exception -> + Log.e("OngoingCall", "Error: ${exception.message}") + } +) +``` + + + + +### ViewModel Events (`OngoingCallEvent`) + +| Event | Fires when | +| --- | --- | +| `CallEnded` | This device left the session (End tapped, remote hang-up, connection closed) | +| `SessionTimeout` | The session timed out | +| `UserJoined(userId, isCurrentUser)` | A participant joined | +| `UserLeft(userId)` | A participant left | +| `Error(exception)` | Join failed or a permission is missing | + +### Global UI Events (CometChatCallEvents) + +| Event | Fires when | Payload | +| --- | --- | --- | +| `ccCallEnded` | A 1:1 call ends on this device | `Call` (the answered call, when the screen was opened by the Push SDK) | + +### SDK Events (Real-Time, Automatic) + +| Calls SDK listener | Internal behavior | +| --- | --- | +| `SessionStatusListener.onSessionLeft` | Local clean-up, `CallEnded` | +| `SessionStatusListener.onSessionTimedOut` | `SessionTimeout`, screen ends | +| `SessionStatusListener.onConnectionClosed` | Screen ends, `CallEnded` (a join whose connection closed without a leave event) | +| `ParticipantEventListener.onParticipantJoined / Left` | `UserJoined` / `UserLeft` | +| `ButtonClickListener.onLeaveSessionButtonClicked` | The Calls SDK has already left; local clean-up only | + +--- + +## Functionality + +| Method (Kotlin XML) | Compose Parameter | Description | +| --- | --- | --- | +| `setSessionId(id)` | `sessionId` | Session to join (required) | +| `setCallType(type)` | `callType` | `CometChatConstants.CALL_TYPE_AUDIO` or `CALL_TYPE_VIDEO`; sets the session type | +| `setCallWorkFlow(flow)` | `callWorkFlow` | `DEFAULT` for 1:1 calls, `MEETING` for group calls | +| `setSessionSettingsBuilder(builder)` | `callSettingsBuilder` | Calls SDK `SessionSettingsBuilder` for layout, controls, audio mode. The component sets the session type on it | +| `startCall()` | automatic | Joins the session | +| `setOnError { }` | `onError` | Error callback | +| `setStyle(style)` | `style` | Container styling | +| `getViewModel()` | `viewModel` | Access to state and events | + +--- + +## Hang-up and clean-up + +Hanging up leaves the Calls SDK session with `CallSession.leave()`; nothing is sent to the Chat SDK. For a 1:1 call (`DEFAULT`) the UI Kit also clears its active-call state and emits `ccCallEnded`. For a `MEETING` it only leaves and clears the meeting flag; the conference continues for the others. + +`CometChatOngoingCallActivity` leaves the session when it is destroyed, but only its **own** session: after answering a second call over a live one, the Push SDK opens a fresh activity for the new session (the answer intent arrives in `onNewIntent` with `SINGLE_TOP`), and the old screen's clean-up does not touch it. + +--- + +## Picture-in-Picture + +`CometChatOngoingCallActivity` declares `supportsPictureInPicture` and enters PiP when the user leaves the screen during a call; the Calls SDK switches to its PiP layout (`enablePictureInPictureLayout` / `disablePictureInPictureLayout`). Dismissing the PiP window ends the call. Nothing to configure. + +--- + +## Second call during a call + +Handled by the Push SDK: the new call rings over the live one with "Answering ends your current call". Answer leaves the live session, prepares the new one and opens `CometChatOngoingCallActivity` again for it; Decline sends `busy` and keeps the current call. + +--- + +## Style + + + + +```kotlin lines +val style = CometChatOngoingCallStyle.Builder(this) + .setBackgroundColor(Color.parseColor("#141414")) + .setProgressIndicatorColor(Color.parseColor("#6852D6")) + .setCornerRadius(0f) + .build() + +CometChatOngoingCallActivity.launchOngoingCallActivity( + context = this, + sessionId = sessionId, + callType = callType, + style = style +) +``` + + + + +```kotlin lines +CometChatOngoingCall( + sessionId = sessionId, + callType = callType, + style = CometChatOngoingCallStyle.default().copy( + backgroundColor = Color(0xFF141414), + progressIndicatorColor = Color(0xFF6852D6) + ) +) +``` + + + + +The style covers the container around the call surface (background, progress indicator while joining, corner radius, stroke). The call UI itself (tiles, buttons, layouts) is the Calls SDK's and is configured through its `SessionSettingsBuilder`; see [Session Settings](/calls/android/session-settings) and [Call Layouts](/calls/android/call-layouts). + +See [Component Styling](/ui-kit/android/component-styling) for the full reference. + +--- + +## Next Steps + + + + Enable calling and push ringing + + + Calling and ringing states before the answer + + + View call history + + + How incoming calls ring + + diff --git a/ui-kit/android/outgoing-call.mdx b/ui-kit/android/outgoing-call.mdx index ece476265..75e4120c6 100644 --- a/ui-kit/android/outgoing-call.mdx +++ b/ui-kit/android/outgoing-call.mdx @@ -1,9 +1,11 @@ --- title: "Outgoing Call" -description: "Full-screen outgoing call UI with recipient avatar, name, call status, and end-call button." +description: "Full-screen outgoing call UI with recipient avatar, name, calling and ringing status, end-call button, and the reason when a call is declined, busy or unanswered." --- -`CometChatOutgoingCall` renders the outgoing call screen and transitions to the ongoing call screen when the receiver accepts. It displays the recipient's avatar, name, call status, and an end-call button. +`CometChatOutgoingCall` renders the outgoing call screen from the moment a call is placed until the callee joins, declines, is busy or does not answer. It displays the recipient's avatar, name, the call status ("calling…", then "ringing…" once the callee's device is reached) and an end-call button, and transitions to the ongoing call screen when the callee joins. + +The screen listens on the Calls SDK session that `initiateCall` prepared, without joining it. The caller joins only when the callee has joined, so no media runs until there is someone to talk to. The Chat SDK is not involved. @@ -13,7 +15,7 @@ description: "Full-screen outgoing call UI with recipient avatar, name, call sta ## Where It Fits -`CometChatOutgoingCall` is a call-initiation component. Wire it to `CometChatCallEvents` to handle call-accepted and call-rejected transitions. +`CometChatOutgoingCall` is a call-initiation component. [Call Buttons](/ui-kit/android/call-buttons), [Call Logs](/ui-kit/android/call-logs) and `CometChatCallActivity.launchOutgoingCallScreen` open it for you with the `Call` returned by `CallInitiationUtils.initiateUserCall`; you only place it yourself when you host the outgoing screen in your own activity. @@ -85,7 +87,22 @@ fun OutgoingCallScreen() { -Prerequisites: CometChat SDK initialized with `CometChatUIKit.init()`, a user logged in, and the UI Kit dependency added. +Prerequisites: `CometChatUIKit.init()` with `setEnableCalling(true)`, a user logged in, and a `Call` obtained from `CallInitiationUtils.initiateUserCall` (the session must already be prepared when the screen opens). + +When you host the component yourself, give it a lifecycle owner so it can attach to the prepared session; the XML view uses its hosting activity, and the composable uses `LocalLifecycleOwner`. + +--- + +## Call states + +| State | Subtitle | What happened | +| --- | --- | --- | +| Calling | "calling …" | `initiateCall` succeeded; the backend is pushing the callee | +| Ringing | "ringing …" | The callee's device received the push and pre-warmed the session (`onParticipantConnected`) | +| Accepted | — | The callee joined (`onParticipantJoined`). The End button is disabled and the ongoing call screen opens | +| Ended | — | The call ended without an answer. `endReason` on the ViewModel says why: `rejected`, `busy`, `unanswered`, `cancelled` or `ended` | + +A call nobody answers ends after 45 seconds on both sides: the callee's Push SDK sends `unanswered`, and this screen's own timer ends the call with the same status in case that push is late. Tapping End sends `cancelled` to the callee, whose ring stops. --- @@ -95,7 +112,7 @@ Prerequisites: CometChat SDK initialized with `CometChatUIKit.init()`, a user lo #### `onEndCallClick` -Fires when the end-call button is tapped. Default: cancels the outgoing call via the SDK. +Fires when the end-call button is tapped. Default: cancels the call with `CometChatCalls.endCall(sessionId, "cancelled")`, closes the screen and emits `ccCallRejected`. The button is disabled after the first tap, and a callee answer that arrives after End is not joined. @@ -147,19 +164,33 @@ CometChatOutgoingCall( +#### `onCallAccepted` (Compose) + +Fires when the callee joins. Default: launches `CometChatOngoingCallActivity` and finishes the hosting activity. Provide the callback to host the call screen yourself. + +```kotlin lines +CometChatOutgoingCall( + call = call, + onCallAccepted = { accepted -> /* open your own call screen for accepted.sessionId */ } +) +``` + ### Global UI Events (CometChatCallEvents) | Event | Fires when | Payload | | --- | --- | --- | -| `ccCallAccepted` | Outgoing call accepted by receiver | `Call` | -| `ccCallRejected` | Outgoing call rejected by receiver | `Call` | +| `ccCallRejected` | The screen closed without an answer: End tapped, or the call was declined, busy, unanswered or cancelled elsewhere | `Call` | ### SDK Events (Real-Time, Automatic) -| SDK Listener | Internal behavior | +| Calls SDK listener | Internal behavior | | --- | --- | -| `onOutgoingCallAccepted` | Transitions to the ongoing call screen | -| `onOutgoingCallRejected` | Finishes the activity or invokes back-press handler | +| `ParticipantEventListener.onParticipantConnected` (prepared session) | Subtitle changes to "ringing …" | +| `ParticipantEventListener.onParticipantJoined` (prepared session) | Accepted: End disabled, ongoing call screen opens and joins | +| `SessionStatusListener.onConnectionClosed` / `onSessionTimedOut` (prepared session) | Waits 1.5 s for the reason from the terminal push, then closes as `ended` | +| `IncomingCallListener.onIncomingCallEnded(call, reason)` | Closes with `rejected`, `busy`, `unanswered` or `cancelled` | + +The Chat SDK's `CallListener` is deprecated and not used. --- @@ -169,9 +200,37 @@ CometChatOutgoingCall( | --- | --- | --- | | `setCall(call)` | `call = call` | Set the Call object (required) | | `setOnEndCallClickListener { }` | `onEndCallClick = { }` | Override end-call button behavior | +| — | `onCallAccepted = { }` | Override what happens when the callee joins | | `setOnError { }` | `onError = { }` | Error callback | -| `setDisableSoundForCalls(true)` | `disableSoundForCalls = true` | Disable outgoing call ringtone | -| `setCustomSoundForCalls(R.raw.tone)` | `customSoundForCalls = R.raw.tone` | Custom ringtone | +| `setOnBackPressListener { }` | `onBackPress = { }` | Back press handling | +| `setCallSettingsBuilder(builder)` | `callSettingsBuilder` | `SessionSettingsBuilder` handed to the ongoing call screen on accept | +| `setDisableSoundForCalls(true)` | `disableSoundForCalls = true` | Disable the outgoing ringback tone | +| `setCustomSoundForCalls(R.raw.tone)` | `customSoundForCalls = R.raw.tone` | Custom ringback tone | +| `getViewModel()` | `viewModel` | Access to the state flows below | + +### ViewModel (`CometChatOutgoingCallViewModel`) + +| Member | Description | +| --- | --- | +| `call`, `acceptedCall`, `rejectedCall` | `StateFlow`: the current call, the call once the callee joined, the call once it ended without an answer | +| `endReason` | `StateFlow`: why `rejectedCall` was emitted (`UIKitConstants.CallStatusConstants.REJECTED`, `BUSY`, `UNANSWERED`, `CANCELLED`, `ENDED`); null while ringing or accepted | +| `calleeRinging` | `StateFlow`: true once the callee's device is reached | +| `endCallButtonEnabled` | `StateFlow`: false after End is tapped or the call is accepted | +| `errorEvent` | `SharedFlow` | +| `ringTimeoutMs` | How long to ring before this side ends the call as unanswered. Default `DEFAULT_RING_TIMEOUT_MS` (45 000); 0 or less disables the timer | +| `closeGraceMs` | How long to wait after the prepared session closes for the terminal push that carries the reason. Default `DEFAULT_CLOSE_GRACE_MS` (1 500) | +| `cancelCall()` | Ends the call as `cancelled` | +| `attachPreparedSession(owner)` | Listens on the prepared session; the component calls it for you | + +Showing the reason, for example a "Busy" label before the screen closes: + +```kotlin lines +lifecycleScope.launch { + outgoingCall.getViewModel()?.endReason?.collect { reason -> + if (reason == UIKitConstants.CallStatusConstants.BUSY) showBusy() + } +} +``` --- diff --git a/ui-kit/android/sound-manager.mdx b/ui-kit/android/sound-manager.mdx index 92e7e846c..e34bb36c9 100644 --- a/ui-kit/android/sound-manager.mdx +++ b/ui-kit/android/sound-manager.mdx @@ -24,8 +24,8 @@ description: "Play, pause, and customize audio cues for calls and messages using | Sound | When it plays | | --- | --- | -| `Sound.INCOMING_CALL` | Incoming call received (with vibration) | -| `Sound.OUTGOING_CALL` | Outgoing call initiated | +| `Sound.INCOMING_CALL` | Only without the Push Notifications SDK calls module. With it, the incoming ringtone is the device ringtone on the Push SDK's `incoming_calls_v1` channel, and the UI Kit plays nothing | +| `Sound.OUTGOING_CALL` | Outgoing call initiated (ringback on the caller's screen) | | `Sound.INCOMING_MESSAGE` | Message received in the active conversation | | `Sound.OUTGOING_MESSAGE` | Message sent | | `Sound.INCOMING_MESSAGE_FROM_OTHER` | Message received from a different conversation | @@ -67,7 +67,7 @@ soundManager.release() ## Disable Sounds on Components -Both UI Kit modules let you disable sounds on individual components. +Both UI Kit modules let you disable sounds on individual components. For calls, `setDisableSoundForCalls` and `setCustomSoundForCalls` apply to the outgoing ringback tone; the incoming ringtone belongs to the Push Notifications SDK and is changed through the notification channel, not here. diff --git a/ui-kit/android/troubleshooting.mdx b/ui-kit/android/troubleshooting.mdx index 1bf4ee2d8..c376382d4 100644 --- a/ui-kit/android/troubleshooting.mdx +++ b/ui-kit/android/troubleshooting.mdx @@ -78,15 +78,22 @@ description: "Common failure modes and fixes for the CometChat Android V6 UI Kit | Symptom | Cause | Fix | | --- | --- | --- | -| Call buttons not appearing | `com.cometchat:calls-sdk-android` not installed | Add the calls SDK dependency to your `build.gradle` — the UI Kit auto-detects it. See [Integration](/ui-kit/android/getting-started) | -| Incoming call screen not showing | `CometChatIncomingCall` not added to the Activity | Render `CometChatIncomingCall` at the root of your Activity layout so it can listen for incoming calls | -| Incoming calls not detected | Call events not observed | Ensure you collect from `CometChatEvents.onIncomingCall` SharedFlow in your Application class or relevant scope | -| Call connects but no audio/video | Missing runtime permissions | Request `CAMERA`, `RECORD_AUDIO`, and `INTERNET` permissions at runtime before initiating calls | -| Call fails immediately | Calls SDK version mismatch | Ensure `com.cometchat:calls-sdk-android` version is compatible with your UI Kit version. Check the [Integration](/ui-kit/android/getting-started) page for version pairing | -| Sound still plays after disabling | `disableSoundForCalls(true)` called too late | Call `disableSoundForCalls(true)` before the component starts the call. If called after the call is initiated, it may not take effect | -| Custom call sound not playing | Audio file missing or wrong path | Verify the sound file exists in `res/raw` and pass the correct `@RawRes int` resource ID to `setCustomSoundForCalls` | -| Call events not received | Not collecting from SharedFlow | Use `CometChatEvents` singleton and collect from the appropriate SharedFlow (e.g., `onCallEnded`, `onIncomingCall`) in a coroutine scope | -| Call events still firing after screen closed | Coroutine scope not cancelled | Cancel the coroutine scope collecting from `CometChatEvents` SharedFlows in `onDestroy()` or `onDestroyView()` | +| Call buttons not appearing | `enableCalling` not set | `setEnableCalling(true)` in `UIKitSettings` (or `"enableCalling": true` in `cometchat-settings.json`). See [Calling Integration](/ui-kit/android/calling-integration) | +| Call logs show a silent error after enabling calling | User logged in before calling was enabled, so the Calls SDK never logged in | `CometChatUIKit.logout()` then `CometChatUIKit.login()` once | +| Incoming calls never ring | Push ringing not configured, or the push not handed to the SDK | `CometChatPushNotifications.init` with `setProviderId` + `setAnswerActivity(CometChatOngoingCallActivity::class.java)`, and `handlePushNotification(this, data, sentTime)` for every push in your `FirebaseMessagingService`. See [Push Ringing](/calls/android/voip-calling) | +| Rings on an emulator or a fresh install, but not on a user's phone | Device settings silence the ring | Run `CometChatPushNotifications.ensureRingReady(activity)` after sign-in and show any result other than `READY` (notifications off, muted channel, full-screen intent revoked, Do Not Disturb) | +| Rings, then the process dies before Answer | Call push sent at normal FCM priority | Send call pushes at high priority | +| No push on a staging or dedicated deployment | Push token registered on the default host | Pass the same `overrideClientHost` to `PNConfiguration` as to `UIKitSettings` | +| Answer opens the app but the call does not join | Answer activity not the UI Kit's, or SDK not initialised on a cold start | Register `CometChatOngoingCallActivity` as the answer activity and run `CometChatUIKit.init` in `Application.onCreate` from stored credentials | +| Caller's screen stays on "calling…" and never shows "ringing…" | Callee's device not reached (push not delivered) | Check the callee's `ensureRingReady`, provider id and host; the caller times out as unanswered after 45 s | +| Audio call opens the caller's camera | Custom call start without a session type | Place calls with `CallInitiationUtils.initiateUserCall`, or set `setSessionType(SessionType.VOICE)` in the `InitiateCallOptions` session settings | +| Ring keeps playing after logout, or declines fail after switching users | Logged out with `CometChat.logout` only | Use `CometChatUIKit.logout()` | +| Two ringtones or two incoming screens | Legacy `CometChat.addCallListener` + `CometChatIncomingCall` overlay still in the app | Remove them; the Push SDK rings. See [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing) | +| Call connects but no audio/video | Missing runtime permissions | Declare `CAMERA` and `RECORD_AUDIO` in the manifest; the Push SDK asks on Answer and the call screen asks when it opens. A denied microphone joins muted | +| Sound still plays after disabling | `disableSoundForCalls(true)` called too late, or it is the incoming ringtone | `setDisableSoundForCalls` only controls the outgoing ringback; the incoming ringtone is the Push SDK's notification channel | +| Custom call sound not playing | Audio file missing or wrong path | Verify the sound file exists in `res/raw` and pass the correct `@RawRes int` resource ID to `setCustomSoundForCalls` (outgoing ringback only) | +| Call events not received | Listening on the wrong SDK | Listen with `CometChatCalls.addIncomingCallListener` (a call starting or ending), or collect `CometChatEvents` call events (`ccOutgoingCall`, `ccCallRejected`, `ccCallEnded`). The Chat SDK `CallListener` is deprecated and fires nothing in this flow | +| Call events still firing after screen closed | Listener or coroutine scope not removed | `removeIncomingCallListener(id)` and cancel the collecting scope in `onDestroy()` or `onDestroyView()` | --- diff --git a/ui-kit/android/upgrading-from-v5.mdx b/ui-kit/android/upgrading-from-v5.mdx index fe512060e..cd74d49cc 100644 --- a/ui-kit/android/upgrading-from-v5.mdx +++ b/ui-kit/android/upgrading-from-v5.mdx @@ -368,6 +368,22 @@ import com.cometchat.uikit.kotlin.presentation.callbuttons.CometChatCallButtons import com.cometchat.uikit.kotlin.presentation.calllogs.ui.CometChatCallLogs ``` +#### Ringing + +V6 rings incoming calls through the CometChat Push Notifications SDK instead of the Chat SDK's `CallListener`. What changes in your app: + +| V5 | V6 | +| --- | --- | +| `CometChat.addCallListener(...)` in the Application class showing `CometChatIncomingCall` | Remove it. The Push SDK rings and draws the incoming-call screen in every app state | +| Push SDK 1.0 `setVoIPEnabled(true)` + `setOnCallAnsweredHandler` (which called `CometChat.acceptCall`) | `PNConfiguration.Builder(...).setProviderId(...).setAnswerActivity(CometChatOngoingCallActivity::class.java)` | +| `FirebaseMessagingService` that skipped call pushes in the foreground and registered tokens manually | `handlePushNotification(this, data, sentTime)` for every push; tokens are automatic | +| `CometChatIncomingCall` as the primary incoming UI | Deprecated; kept only for `setIncomingCallActivity` | +| `CometChat.logout` | `CometChatUIKit.logout` (also logs out of the Calls SDK) | +| Own ringtone, busy check, ConnectionService stack | Delete; the Push SDK does them | +| `CometChatCallsConstants.*` | `UIKitConstants.CallStatusConstants` / `UIKitConstants.CallType` | + +Add `CAMERA` to the manifest next to `RECORD_AUDIO` and `POST_NOTIFICATIONS`. Full walkthrough: [Calling Integration](/ui-kit/android/calling-integration) and [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). + ## Architecture Changes From edbb5e90d3dfa86e03ca7824d4c781d279dbc22e Mon Sep 17 00:00:00 2001 From: Hritika Date: Wed, 14 Oct 2026 19:45:12 +0530 Subject: [PATCH 2/7] docs(android): generic Calls overview diagram; drop the Chat SDK line from Ringing Co-Authored-By: Claude Fable 5.1 --- calls/android/overview.mdx | 8 +++++--- calls/android/ringing.mdx | 2 -- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/calls/android/overview.mdx b/calls/android/overview.mdx index ad2643e05..f44cba28b 100644 --- a/calls/android/overview.mdx +++ b/calls/android/overview.mdx @@ -28,15 +28,17 @@ sequenceDiagram App->>CometChatCalls: init() App->>CometChatCalls: login() - App->>CometChatCalls: initiateCall() — caller, or answer from the push ring — callee - App->>CometChatCalls: joinSession() + App->>CometChatCalls: joinSession(sessionId) CometChatCalls-->>App: CallSession App->>CallSession: Actions (mute, pause, etc.) CallSession-->>App: Event callbacks App->>CallSession: leave() ``` -Ringing the other user is done by the Push Notifications SDK's calls module, not by the Chat SDK: see [Ringing](/calls/android/ringing) and [Push Ringing](/calls/android/voip-calling). A session can also be joined directly by id for a group call or a meeting. +Every call is a session joined by id. Where the id comes from depends on the kind of call: + +- **Direct call**: your app shares a session id (for example in a message) and each participant calls `joinSession` with it. Group calls and meetings work this way. +- **Ringing call**: the caller starts it with `CometChatCalls.initiateCall`, the Push Notifications SDK rings the callee, and both sides join the session once the callee answers. See [Ringing](/calls/android/ringing) and [Push Ringing](/calls/android/voip-calling). ## Features diff --git a/calls/android/ringing.mdx b/calls/android/ringing.mdx index 1dfa4276a..a2dadf3bb 100644 --- a/calls/android/ringing.mdx +++ b/calls/android/ringing.mdx @@ -14,8 +14,6 @@ Two SDKs do the work: | **Calls SDK** (`calls-sdk-android` 5.1+) | Creating the call, the media session, answering, declining, cancelling, ending, and the call events your code listens to | | **Push Notifications SDK, calls module** (`push-notifications-calls-android` 2.0+) | Ringing the callee's device: the call notification, the full-screen incoming-call screen, the ringtone, the ring timeout, missed calls and busy handling | -The Chat SDK is not part of ringing. It is still needed for chat, but nothing on this page calls it. - **Coming from the earlier ringing flow?** The Chat SDK's calling methods (`CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `CallListener`) are deprecated and are replaced by what this page describes. See [Migrating from legacy ringing](/calls/android/migrating-from-legacy-ringing) for a method-by-method mapping. From e4ec8af69bc582e590171f0cc65153031b732111 Mon Sep 17 00:00:00 2001 From: Hritika Date: Thu, 15 Oct 2026 11:29:36 +0530 Subject: [PATCH 3/7] docs(android): address review: restore onDominantSpeakerChanged, Chat SDK calling is superseded not deprecated, real call event names, getting-started parity --- calls/android/events.mdx | 2 ++ .../android/migrating-from-legacy-ringing.mdx | 4 ++-- calls/android/migration-guide-v5.mdx | 2 +- calls/android/ringing.mdx | 2 +- calls/android/voip-calling.mdx | 2 +- notifications/android-push-notifications.mdx | 4 ++-- sdk/android/v5/calling-overview.mdx | 4 ++-- sdk/android/v5/real-time-listeners.mdx | 2 +- ui-kit/android/call-buttons.mdx | 8 +++---- ui-kit/android/call-features.mdx | 4 ++-- ui-kit/android/calling-integration.mdx | 2 +- ui-kit/android/getting-started.mdx | 24 +++++++------------ ui-kit/android/incoming-call.mdx | 8 +++---- ui-kit/android/llms-android-v6.mdx | 2 +- ui-kit/android/ongoing-call.mdx | 6 ++--- ui-kit/android/outgoing-call.mdx | 8 +++---- ui-kit/android/troubleshooting.mdx | 2 +- 17 files changed, 40 insertions(+), 46 deletions(-) diff --git a/calls/android/events.mdx b/calls/android/events.mdx index 93e735db7..7f950ffcf 100644 --- a/calls/android/events.mdx +++ b/calls/android/events.mdx @@ -154,6 +154,7 @@ callSession.addParticipantEventListener(this, object : ParticipantEventListener( override fun onParticipantStoppedScreenShare(participant: Participant) {} override fun onParticipantStartedRecording(participant: Participant) {} override fun onParticipantStoppedRecording(participant: Participant) {} + override fun onDominantSpeakerChanged(participant: Participant) {} }) ``` @@ -203,6 +204,7 @@ callSession.addParticipantEventListener(this, new ParticipantEventListener() { | `onParticipantStoppedScreenShare` | `Participant` | A participant stopped screen sharing | | `onParticipantStartedRecording` | `Participant` | A participant started recording | | `onParticipantStoppedRecording` | `Participant` | A participant stopped recording | +| `onDominantSpeakerChanged` | `Participant` | The active speaker changed | --- diff --git a/calls/android/migrating-from-legacy-ringing.mdx b/calls/android/migrating-from-legacy-ringing.mdx index 7b1dc7ce2..d3fc62e0d 100644 --- a/calls/android/migrating-from-legacy-ringing.mdx +++ b/calls/android/migrating-from-legacy-ringing.mdx @@ -5,7 +5,7 @@ sdk_version: "5.x" description: "Move an Android app from Chat SDK call signalling and Push Notifications SDK 1.0 VoIP to ringing with the Calls SDK 5.1 and the Push Notifications SDK 2.0: what to add, what to delete, and how each method maps." --- -Legacy ringing signalled calls through the Chat SDK: `CometChat.initiateCall` created the call, a `CallListener` on the WebSocket told the callee, `acceptCall` and `rejectCall` answered, and the app rang the device itself, often with a hand-built `ConnectionService`. Ringing now runs on the Calls SDK and the Push Notifications SDK calls module. The Chat SDK's calling methods are deprecated. +Legacy ringing signalled calls through the Chat SDK: `CometChat.initiateCall` created the call, a `CallListener` on the WebSocket told the callee, `acceptCall` and `rejectCall` answered, and the app rang the device itself, often with a hand-built `ConnectionService`. Ringing now runs on the Calls SDK and the Push Notifications SDK calls module. The Chat SDK's calling methods are superseded by the Calls SDK and no longer used; a coming Chat SDK release marks them deprecated. This page is the diff. For the full description of the current flow see [Ringing](/calls/android/ringing); for the push side see [Push ringing](/calls/android/voip-calling). @@ -120,7 +120,7 @@ override fun onMessageReceived(message: RemoteMessage) { if (CometChatPushNotifications.isAppInForeground() && isCallPush(message.data)) { return // the WebSocket CallListener handles it } - CometChatPushNotifications.handlePushNotification(this, message.data) + CometChatPushNotifications.handlePushNotification(this, title, body, icon, uid, guid, message.data) } override fun onNewToken(token: String) { diff --git a/calls/android/migration-guide-v5.mdx b/calls/android/migration-guide-v5.mdx index 45f6d3900..d8b7766bb 100644 --- a/calls/android/migration-guide-v5.mdx +++ b/calls/android/migration-guide-v5.mdx @@ -475,7 +475,7 @@ These classes still exist in v5 for backward compatibility but are deprecated: |-------------------|-------------| | `CallSession.leaveSession()` | `CallSession.leave()` | | `CometChatCallsConstants` (the whole class) | `com.cometchat.calls.constants.Constants` | -| Chat SDK calling: `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall`, `addCallListener`, `CallListener`, `CallSettings` | `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `addIncomingCallListener`, the Push Notifications SDK calls module, `leave()` | +| Chat SDK calling (superseded; deprecated in a coming Chat SDK release): `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall`, `addCallListener`, `CallListener`, `CallSettings` | `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `addIncomingCallListener`, the Push Notifications SDK calls module, `leave()` | New in 5.1: `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `connect`, `getActiveSession`, `addIncomingCallListener` / `IncomingCallListener`, `addLoginListener` / `LoginListener`, `ParticipantEventListener.onParticipantConnected`, `CallAppSettingBuilder.overrideAdminHost / overrideClientHost / overrideCallsHost`. diff --git a/calls/android/ringing.mdx b/calls/android/ringing.mdx index a2dadf3bb..472376a62 100644 --- a/calls/android/ringing.mdx +++ b/calls/android/ringing.mdx @@ -15,7 +15,7 @@ Two SDKs do the work: | **Push Notifications SDK, calls module** (`push-notifications-calls-android` 2.0+) | Ringing the callee's device: the call notification, the full-screen incoming-call screen, the ringtone, the ring timeout, missed calls and busy handling | -**Coming from the earlier ringing flow?** The Chat SDK's calling methods (`CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `CallListener`) are deprecated and are replaced by what this page describes. See [Migrating from legacy ringing](/calls/android/migrating-from-legacy-ringing) for a method-by-method mapping. +**Coming from the earlier ringing flow?** The Chat SDK's calling methods (`CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `CallListener`) are superseded by the Calls SDK and no longer used; a coming Chat SDK release marks them deprecated. This page describes the replacement. See [Migrating from legacy ringing](/calls/android/migrating-from-legacy-ringing) for a method-by-method mapping. ## How ringing works diff --git a/calls/android/voip-calling.mdx b/calls/android/voip-calling.mdx index 02d9b1148..1a793ac5c 100644 --- a/calls/android/voip-calling.mdx +++ b/calls/android/voip-calling.mdx @@ -77,7 +77,7 @@ CometChatPushNotifications.init( -A calls-only app can start from the settings it already has: `PNConfiguration.from(callAppSettings)` in Kotlin, `CometChatCallPushConfig.from(callAppSettings)` in Java. +A calls-only app can start from the settings it already has: `PNConfiguration.from(callAppSettings)` in Kotlin (an extension in the calls module: `import com.cometchat.pushnotification.calls.from`), `CometChatCallPushConfig.from(callAppSettings)` in Java. | Setter | Default | What it does | |--------|---------|--------------| diff --git a/notifications/android-push-notifications.mdx b/notifications/android-push-notifications.mdx index a3f34d759..a38452a94 100644 --- a/notifications/android-push-notifications.mdx +++ b/notifications/android-push-notifications.mdx @@ -58,8 +58,8 @@ Two CometChat artifacts, published in lockstep. The calls one is optional: add i ```toml lines [versions] minSdk = "26" -compileSdk = "35" -targetSdk = "35" +compileSdk = "36" +targetSdk = "36" cometChatUikit = "6.1.0" cometChatPush = "2.0.0" firebaseBom = "33.7.0" diff --git a/sdk/android/v5/calling-overview.mdx b/sdk/android/v5/calling-overview.mdx index fae822120..9e159bf9c 100644 --- a/sdk/android/v5/calling-overview.mdx +++ b/sdk/android/v5/calling-overview.mdx @@ -1,13 +1,13 @@ --- title: "Calling" sidebarTitle: "Calling" -description: "Voice and video calling for Android: the Calls SDK owns calls and ringing, the Push Notifications SDK rings the device, and the Chat SDK's calling methods are deprecated." +description: "Voice and video calling for Android: the Calls SDK owns calls and ringing, the Push Notifications SDK rings the device, and the Chat SDK's calling methods are superseded." --- Voice and video calling on Android is provided by the CometChat Calls SDK, with incoming calls rung by the Push Notifications SDK. The Chat SDK is used for chat; it no longer takes part in calling. -**The Chat SDK's calling APIs are deprecated.** `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall`, `getCallParticipantCount`, `addCallListener` / `removeCallListener`, `CallListener`, `OngoingCallListener` and `CallSettings` still compile but are replaced by the Calls SDK: `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `joinSession`, `CallSession.leave()` and `addIncomingCallListener`. Ringing the callee is done by the Push Notifications SDK calls module. See [Ringing](/calls/android/ringing) and [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). +**The Chat SDK's calling APIs are superseded.** `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall`, `getCallParticipantCount`, `addCallListener` / `removeCallListener`, `CallListener`, `OngoingCallListener` and `CallSettings` still work, and a coming Chat SDK release marks them `@Deprecated`. Their replacement is the Calls SDK: `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `joinSession`, `CallSession.leave()` and `addIncomingCallListener`. Ringing the callee is done by the Push Notifications SDK calls module. See [Ringing](/calls/android/ringing) and [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). Not deprecated: the `Call` message model and its getters, `CATEGORY_CALL`, the `CALL_STATUS_*` and `CALL_TYPE_*` constants, because call messages in a conversation's history still use them. diff --git a/sdk/android/v5/real-time-listeners.mdx b/sdk/android/v5/real-time-listeners.mdx index c0e38d41d..fac45a354 100644 --- a/sdk/android/v5/real-time-listeners.mdx +++ b/sdk/android/v5/real-time-listeners.mdx @@ -491,7 +491,7 @@ The `AIAssistantListener` delivers real-time streaming events. After a run compl ## Call Listener -**Deprecated.** `CallListener` and `addCallListener()` are deprecated together with the Chat SDK's other calling methods. Calls now ring through the Push Notifications SDK and announce themselves on the Calls SDK's `CometChatCalls.addIncomingCallListener` (`onIncomingCallReceived`, `onIncomingCallEnded(call, reason)`), whether the call arrived by push on a killed process or while the app was running. The caller's "answered" event is `ParticipantEventListener.onParticipantJoined` on the prepared session. See [Ringing](/calls/android/ringing) and [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). The section below is kept for apps still on the legacy flow. +**Superseded.** `CallListener` and `addCallListener()` are no longer the way calls announce themselves, and a coming Chat SDK release marks them deprecated together with the Chat SDK's other calling methods. Calls now ring through the Push Notifications SDK and announce themselves on the Calls SDK's `CometChatCalls.addIncomingCallListener` (`onIncomingCallReceived`, `onIncomingCallEnded(call, reason)`), whether the call arrived by push on a killed process or while the app was running. The caller's "answered" event is `ParticipantEventListener.onParticipantJoined` on the prepared session. See [Ringing](/calls/android/ringing) and [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). The section below is kept for apps still on the legacy flow. The `CallListener` class provides you with live events related to calls. Below are the callback methods provided by the `CallListener` class. diff --git a/ui-kit/android/call-buttons.mdx b/ui-kit/android/call-buttons.mdx index dd7638f1d..35b42e344 100644 --- a/ui-kit/android/call-buttons.mdx +++ b/ui-kit/android/call-buttons.mdx @@ -220,13 +220,13 @@ CometChatCallButtons( -### Global UI Events (CometChatCallEvents) +### Global UI Events (`CometChatEvents.callEvents`) | Event | Fires when | Payload | | --- | --- | --- | -| `ccOutgoingCall` | An outgoing 1:1 call was created | `Call` | -| `ccCallRejected` | The outgoing screen closed without an answer (End tapped, declined, busy, unanswered, cancelled) | `Call` | -| `ccCallEnded` | A 1:1 call ended on this device | `Call` | +| `CometChatCallEvent.OutgoingCall` | An outgoing 1:1 call was created | `Call` | +| `CometChatCallEvent.CallRejected` | The outgoing screen closed without an answer (End tapped, declined, busy, unanswered, cancelled) | `Call` | +| `CometChatCallEvent.CallEnded` | A 1:1 call ended on this device | `Call` | ### SDK Events (Real-Time, Automatic) diff --git a/ui-kit/android/call-features.mdx b/ui-kit/android/call-features.mdx index 91e027c30..fd6eb3127 100644 --- a/ui-kit/android/call-features.mdx +++ b/ui-kit/android/call-features.mdx @@ -13,7 +13,7 @@ description: "Add one-on-one and group audio/video calling to your Android app u | Incoming calls | Rung by the Push Notifications SDK (notification, full-screen screen, ringtone), in every app state. No call listener, no incoming-call component | | Call features | Call Buttons, Outgoing Call, Ongoing Call, Call Logs, Call Log Details | | Key components | `CometChatCallButtons`, `CometChatOutgoingCall`, `CometChatOngoingCall` / `CometChatOngoingCallActivity`, `CometChatCallLogs` | -| Deprecated | `CometChatIncomingCall` (kept for a custom ring screen), Chat SDK `CometChat.addCallListener` / `initiateCall` / `acceptCall` / `rejectCall` / `endCall` | +| Deprecated / superseded | `CometChatIncomingCall` (deprecated, kept for a custom ring screen); Chat SDK `CometChat.addCallListener` / `initiateCall` / `acceptCall` / `rejectCall` / `endCall` (superseded, no longer used) | | Related | [Calling Integration](/ui-kit/android/calling-integration), [Push Ringing](/calls/android/voip-calling), [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing), [Call Log Details Guide](/ui-kit/android/guide-call-log-details) | @@ -60,7 +60,7 @@ Once calling is active, the call buttons render in the [MessageHeader](/ui-kit/a -There is no call listener to add. Apps that registered `CometChat.addCallListener` to show `CometChatIncomingCall` should remove it; the Chat SDK's calling APIs are deprecated. See [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). +There is no call listener to add. Apps that registered `CometChat.addCallListener` to show `CometChatIncomingCall` should remove it; the Chat SDK's calling APIs are superseded and no longer used. See [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). ## Call Components diff --git a/ui-kit/android/calling-integration.mdx b/ui-kit/android/calling-integration.mdx index 7c39001a6..3663534cc 100644 --- a/ui-kit/android/calling-integration.mdx +++ b/ui-kit/android/calling-integration.mdx @@ -264,7 +264,7 @@ CometChatMessageHeader( | Call history | Call logs list from the Calls SDK; the call icon calls back | [Call Logs](/ui-kit/android/call-logs) | | Group call | The group call button sends a meeting message; tapping it joins the conference | Call Buttons, Ongoing Call | -To react to calls in your own code (a badge, analytics, refreshing a screen), listen on the Calls SDK with `CometChatCalls.addIncomingCallListener`, the same callback the UI Kit uses internally. Do not use the Chat SDK's deprecated `CallListener`. +To react to calls in your own code (a badge, analytics, refreshing a screen), listen on the Calls SDK with `CometChatCalls.addIncomingCallListener`, the same callback the UI Kit uses internally. Do not use the Chat SDK's `CallListener`; it is superseded and fires nothing in this flow. ## Placing a call from your own screen diff --git a/ui-kit/android/getting-started.mdx b/ui-kit/android/getting-started.mdx index cb38fc392..f03716903 100644 --- a/ui-kit/android/getting-started.mdx +++ b/ui-kit/android/getting-started.mdx @@ -15,7 +15,7 @@ description: "Install, configure, and launch the CometChat Android UI Kit in you | Order | `init()` → `login()` → render. Breaking this order = blank screen | | Auth Key | Dev/testing only. Use Auth Token in production | | Theme | Set `CometChatTheme.DayNight` as parent theme in `themes.xml` | -| Calling | Optional. Add `com.cometchat:calls-sdk-android` to enable voice/video | +| Calling | Optional. `setEnableCalling(true)` in `UIKitSettings` + push ringing setup; the Calls and Push SDKs ship with the UI Kit | | Min SDK | Android 9.0 (API 28) | @@ -88,8 +88,7 @@ Inside `libs.versions.toml`, add the versions: ```toml libs.versions.toml [versions] -cometchat-ui-kit = "6.0.3" -cometchat-calls-sdk = "5.0.1" +cometchat-ui-kit = "6.1.0" ``` Define the libraries — pick the module for your UI toolkit: @@ -99,7 +98,6 @@ Define the libraries — pick the module for your UI toolkit: ```toml libs.versions.toml [libraries] cometchat-ui-kit = { module = "com.cometchat:chatuikit-kotlin-android", version.ref = "cometchat-ui-kit" } -cometchat-calls-sdk = { module = "com.cometchat:calls-sdk-android", version.ref = "cometchat-calls-sdk" } ``` @@ -107,7 +105,6 @@ cometchat-calls-sdk = { module = "com.cometchat:calls-sdk-android", version.ref ```toml libs.versions.toml [libraries] cometchat-ui-kit = { module = "com.cometchat:chatuikit-compose-android", version.ref = "cometchat-ui-kit" } -cometchat-calls-sdk = { module = "com.cometchat:calls-sdk-android", version.ref = "cometchat-calls-sdk" } ``` @@ -117,9 +114,8 @@ Now, in your app-level `build.gradle.kts` file: ```kotlin build.gradle.kts dependencies { implementation(libs.cometchat.ui.kit) - - // (Optional) Include if using voice/video calling features - implementation(libs.cometchat.calls.sdk) + // Voice/video calling needs nothing more here: the Calls SDK and the Push Notifications SDK + // come with the UI Kit. See Calling Integration for push ringing (Firebase Messaging). } ``` @@ -132,10 +128,8 @@ Open the app-level `build.gradle.kts` file and add the dependency for your chose ```kotlin build.gradle.kts dependencies { - implementation("com.cometchat:chatuikit-kotlin-android:6.0.6") - - // (Optional) Include if using voice/video calling features - implementation("com.cometchat:calls-sdk-android:5.0.1") + implementation("com.cometchat:chatuikit-kotlin-android:6.1.0") + // Calling: nothing more to add; see Calling Integration for push ringing } ``` @@ -143,10 +137,8 @@ dependencies { ```kotlin build.gradle.kts dependencies { - implementation("com.cometchat:chatuikit-compose-android:6.0.6") - - // (Optional) Include if using voice/video calling features - implementation("com.cometchat:calls-sdk-android:5.0.1") + implementation("com.cometchat:chatuikit-compose-android:6.1.0") + // Calling: nothing more to add; see Calling Integration for push ringing } ``` diff --git a/ui-kit/android/incoming-call.mdx b/ui-kit/android/incoming-call.mdx index d03e37183..5a1fa7800 100644 --- a/ui-kit/android/incoming-call.mdx +++ b/ui-kit/android/incoming-call.mdx @@ -208,12 +208,12 @@ CometChatIncomingCall( -### Global UI Events (CometChatCallEvents) +### Global UI Events (`CometChatEvents.callEvents`) | Event | Fires when | Payload | | --- | --- | --- | -| `ccCallAccepted` | Accept tapped on this screen | `Call` | -| `ccCallRejected` | Decline tapped on this screen | `Call` | +| `CometChatCallEvent.CallAccepted` | Accept tapped on this screen | `Call` | +| `CometChatCallEvent.CallRejected` | Decline tapped on this screen | `Call` | ### SDK Events (Real-Time, Automatic) @@ -221,7 +221,7 @@ CometChatIncomingCall( | --- | --- | | `IncomingCallListener.onIncomingCallEnded` for this session | The ring ended elsewhere (caller cancelled, answered on another device, timed out): `rejectedCall` is emitted so the screen closes | -The component does not open itself on an incoming call. The Push SDK launches the registered ring activity; the Chat SDK's `CallListener` is deprecated and not used. +The component does not open itself on an incoming call. The Push SDK launches the registered ring activity; the Chat SDK's `CallListener` is superseded and not used. --- diff --git a/ui-kit/android/llms-android-v6.mdx b/ui-kit/android/llms-android-v6.mdx index 2b57a3dd3..3d019dca6 100644 --- a/ui-kit/android/llms-android-v6.mdx +++ b/ui-kit/android/llms-android-v6.mdx @@ -136,7 +136,7 @@ the symptom, not the component. | Toolbar renders **under the status bar** / title collides with the clock | [Conversation + message view](/ui-kit/android/conversation-message-view) · [One-to-one chat](/ui-kit/android/one-to-one-chat) — `enableEdgeToEdge()` needs insets CONSUMED | | Composer crashes / `ActivityResultLauncher not initialized` | [Troubleshooting](/ui-kit/android/troubleshooting) — build the composer in `onCreate` | | Calling crashes: `Please call the CometChatCalls.init()` | [Calling integration](/ui-kit/android/calling-integration) — `setEnableCalling(true)` is missing | -| Incoming calls never ring | [Calling integration](/ui-kit/android/calling-integration) — `CometChatPushNotifications.init` with `setAnswerActivity(CometChatOngoingCallActivity::class.java)` + a `FirebaseMessagingService` that calls `handlePushNotification`; the Chat SDK `CallListener` is deprecated and fires nothing | +| Incoming calls never ring | [Calling integration](/ui-kit/android/calling-integration) — `CometChatPushNotifications.init` with `setAnswerActivity(CometChatOngoingCallActivity::class.java)` + a `FirebaseMessagingService` that calls `handlePushNotification`; the Chat SDK `CallListener` is superseded and fires nothing | | Components render but stay empty | init/login did not resolve — [Getting started (Kotlin)](/ui-kit/android/getting-started-kotlin) / [(Compose)](/ui-kit/android/getting-started-jetpack) | | Keyboard covers the composer | `windowSoftInputMode="adjustResize"` — [One-to-one chat](/ui-kit/android/one-to-one-chat) | | Colours ignored / theme not applied | [Theme introduction](/ui-kit/android/theme-introduction) | diff --git a/ui-kit/android/ongoing-call.mdx b/ui-kit/android/ongoing-call.mdx index dedd0927c..33f2d8048 100644 --- a/ui-kit/android/ongoing-call.mdx +++ b/ui-kit/android/ongoing-call.mdx @@ -214,11 +214,11 @@ CometChatOngoingCall( | `UserLeft(userId)` | A participant left | | `Error(exception)` | Join failed or a permission is missing | -### Global UI Events (CometChatCallEvents) +### Global UI Events (`CometChatEvents.callEvents`) | Event | Fires when | Payload | | --- | --- | --- | -| `ccCallEnded` | A 1:1 call ends on this device | `Call` (the answered call, when the screen was opened by the Push SDK) | +| `CometChatCallEvent.CallEnded` | A 1:1 call ends on this device | `Call` (the answered call, when the screen was opened by the Push SDK) | ### SDK Events (Real-Time, Automatic) @@ -249,7 +249,7 @@ CometChatOngoingCall( ## Hang-up and clean-up -Hanging up leaves the Calls SDK session with `CallSession.leave()`; nothing is sent to the Chat SDK. For a 1:1 call (`DEFAULT`) the UI Kit also clears its active-call state and emits `ccCallEnded`. For a `MEETING` it only leaves and clears the meeting flag; the conference continues for the others. +Hanging up leaves the Calls SDK session with `CallSession.leave()`; nothing is sent to the Chat SDK. For a 1:1 call (`DEFAULT`) the UI Kit also clears its active-call state and emits `CometChatCallEvent.CallEnded` on `CometChatEvents.callEvents`. For a `MEETING` it only leaves and clears the meeting flag; the conference continues for the others. `CometChatOngoingCallActivity` leaves the session when it is destroyed, but only its **own** session: after answering a second call over a live one, the Push SDK opens a fresh activity for the new session (the answer intent arrives in `onNewIntent` with `SINGLE_TOP`), and the old screen's clean-up does not touch it. diff --git a/ui-kit/android/outgoing-call.mdx b/ui-kit/android/outgoing-call.mdx index 75e4120c6..2565870d8 100644 --- a/ui-kit/android/outgoing-call.mdx +++ b/ui-kit/android/outgoing-call.mdx @@ -112,7 +112,7 @@ A call nobody answers ends after 45 seconds on both sides: the callee's Push SDK #### `onEndCallClick` -Fires when the end-call button is tapped. Default: cancels the call with `CometChatCalls.endCall(sessionId, "cancelled")`, closes the screen and emits `ccCallRejected`. The button is disabled after the first tap, and a callee answer that arrives after End is not joined. +Fires when the end-call button is tapped. Default: cancels the call with `CometChatCalls.endCall(sessionId, "cancelled")`, closes the screen and emits `CometChatCallEvent.CallRejected`. The button is disabled after the first tap, and a callee answer that arrives after End is not joined. @@ -175,11 +175,11 @@ CometChatOutgoingCall( ) ``` -### Global UI Events (CometChatCallEvents) +### Global UI Events (`CometChatEvents.callEvents`) | Event | Fires when | Payload | | --- | --- | --- | -| `ccCallRejected` | The screen closed without an answer: End tapped, or the call was declined, busy, unanswered or cancelled elsewhere | `Call` | +| `CometChatCallEvent.CallRejected` | The screen closed without an answer: End tapped, or the call was declined, busy, unanswered or cancelled elsewhere | `Call` | ### SDK Events (Real-Time, Automatic) @@ -190,7 +190,7 @@ CometChatOutgoingCall( | `SessionStatusListener.onConnectionClosed` / `onSessionTimedOut` (prepared session) | Waits 1.5 s for the reason from the terminal push, then closes as `ended` | | `IncomingCallListener.onIncomingCallEnded(call, reason)` | Closes with `rejected`, `busy`, `unanswered` or `cancelled` | -The Chat SDK's `CallListener` is deprecated and not used. +The Chat SDK's `CallListener` is superseded and not used. --- diff --git a/ui-kit/android/troubleshooting.mdx b/ui-kit/android/troubleshooting.mdx index c376382d4..dd4aadf7c 100644 --- a/ui-kit/android/troubleshooting.mdx +++ b/ui-kit/android/troubleshooting.mdx @@ -92,7 +92,7 @@ description: "Common failure modes and fixes for the CometChat Android V6 UI Kit | Call connects but no audio/video | Missing runtime permissions | Declare `CAMERA` and `RECORD_AUDIO` in the manifest; the Push SDK asks on Answer and the call screen asks when it opens. A denied microphone joins muted | | Sound still plays after disabling | `disableSoundForCalls(true)` called too late, or it is the incoming ringtone | `setDisableSoundForCalls` only controls the outgoing ringback; the incoming ringtone is the Push SDK's notification channel | | Custom call sound not playing | Audio file missing or wrong path | Verify the sound file exists in `res/raw` and pass the correct `@RawRes int` resource ID to `setCustomSoundForCalls` (outgoing ringback only) | -| Call events not received | Listening on the wrong SDK | Listen with `CometChatCalls.addIncomingCallListener` (a call starting or ending), or collect `CometChatEvents` call events (`ccOutgoingCall`, `ccCallRejected`, `ccCallEnded`). The Chat SDK `CallListener` is deprecated and fires nothing in this flow | +| Call events not received | Listening on the wrong SDK | Listen with `CometChatCalls.addIncomingCallListener` (a call starting or ending), or collect `CometChatEvents.callEvents` (`CometChatCallEvent.OutgoingCall`, `CallRejected`, `CallEnded`). The Chat SDK `CallListener` is superseded and fires nothing in this flow | | Call events still firing after screen closed | Listener or coroutine scope not removed | `removeIncomingCallListener(id)` and cancel the collecting scope in `onDestroy()` or `onDestroyView()` | --- From 8c0d6ec870bee081f559bed05171f85c20af1207 Mon Sep 17 00:00:00 2001 From: Hritika Date: Thu, 15 Oct 2026 12:37:52 +0530 Subject: [PATCH 4/7] docs(android): update migration instructions to include enabling ringing migration setting --- calls/android/migrating-from-legacy-ringing.mdx | 8 ++++++++ ui-kit/android/calling-integration.mdx | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/calls/android/migrating-from-legacy-ringing.mdx b/calls/android/migrating-from-legacy-ringing.mdx index d3fc62e0d..b5cc8ce24 100644 --- a/calls/android/migrating-from-legacy-ringing.mdx +++ b/calls/android/migrating-from-legacy-ringing.mdx @@ -9,6 +9,14 @@ Legacy ringing signalled calls through the Chat SDK: `CometChat.initiateCall` cr This page is the diff. For the full description of the current flow see [Ringing](/calls/android/ringing); for the push side see [Push ringing](/calls/android/voip-calling). +## Before you start + +Enable the **ringing migration** setting for your app in the CometChat dashboard, then follow the +steps below. If you use the CometChat UI Kit, this setting is the only step; the rest comes with +the UI Kit release (see [Calling Integration](/ui-kit/android/calling-integration)). + +{/* TODO(RC): add the setting's exact name and dashboard path and a screenshot once the toggle ships. */} + ## Why it changed | Legacy | Now | diff --git a/ui-kit/android/calling-integration.mdx b/ui-kit/android/calling-integration.mdx index 3663534cc..b56caf363 100644 --- a/ui-kit/android/calling-integration.mdx +++ b/ui-kit/android/calling-integration.mdx @@ -295,4 +295,4 @@ Strings, drawables, the theme and the layout can be overridden the same way; the ## Upgrading from an app that used `CometChat.addCallListener` -Remove the Application-level `CallListener`, the incoming-call overlay or `launchIncomingCallScreen` call, any ringtone you played, the busy check, and the Push SDK 1.0 `setVoIPEnabled` / `setOnCallAnsweredHandler` wiring, then follow the steps above. The method-by-method mapping is on [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). +Enable the **ringing migration** setting for your app in the CometChat dashboard. Then remove the Application-level `CallListener`, the incoming-call overlay or `launchIncomingCallScreen` call, any ringtone you played, the busy check, and the Push SDK 1.0 `setVoIPEnabled` / `setOnCallAnsweredHandler` wiring, and follow the steps above. The method-by-method mapping is on [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). From 8bd237958757a99469e893889e2150cd1c017df5 Mon Sep 17 00:00:00 2001 From: Hritika Date: Thu, 15 Oct 2026 17:04:13 +0530 Subject: [PATCH 5/7] docs(android): remove staging and host override details Co-Authored-By: Claude Opus 5.5 --- .../android/migrating-from-legacy-ringing.mdx | 4 +-- calls/android/migration-guide-v5.mdx | 2 +- calls/android/setup.mdx | 35 ++----------------- calls/android/troubleshooting.mdx | 1 - calls/android/voip-calling.mdx | 17 ++------- notifications/android-push-notifications.mdx | 6 ---- ui-kit/android/calling-integration.mdx | 22 ------------ ui-kit/android/methods.mdx | 1 - ui-kit/android/troubleshooting.mdx | 1 - 9 files changed, 7 insertions(+), 82 deletions(-) diff --git a/calls/android/migrating-from-legacy-ringing.mdx b/calls/android/migrating-from-legacy-ringing.mdx index b5cc8ce24..62f4e6000 100644 --- a/calls/android/migrating-from-legacy-ringing.mdx +++ b/calls/android/migrating-from-legacy-ringing.mdx @@ -117,8 +117,6 @@ CometChatPushNotifications.init(this, -If your Chat and Calls SDKs use `overrideClientHost` (a staging or dedicated deployment), pass the same host with `PNConfiguration.Builder.overrideClientHost(host)`. Otherwise the push token registers on the default host and no push ever arrives. - ## Step 3: Shrink the Firebase service @@ -307,7 +305,7 @@ Details on the [Push ringing](/calls/android/voip-calling) page. ## Checklist - [ ] Dependencies bumped, `push-notifications-calls-android` added, `minSdk 26` -- [ ] `PNConfiguration` has `setProviderId` and `setAnswerActivity`; `overrideClientHost` matches the Chat and Calls SDKs if you use one +- [ ] `PNConfiguration` has `setProviderId` and `setAnswerActivity` - [ ] Firebase service passes every push to `handlePushNotification` with `message.sentTime` - [ ] Manual token registration removed - [ ] `CometChat.addCallListener`, own incoming UI, ringtone, busy check and the ConnectionService stack removed diff --git a/calls/android/migration-guide-v5.mdx b/calls/android/migration-guide-v5.mdx index d8b7766bb..7970bcb72 100644 --- a/calls/android/migration-guide-v5.mdx +++ b/calls/android/migration-guide-v5.mdx @@ -477,6 +477,6 @@ These classes still exist in v5 for backward compatibility but are deprecated: | `CometChatCallsConstants` (the whole class) | `com.cometchat.calls.constants.Constants` | | Chat SDK calling (superseded; deprecated in a coming Chat SDK release): `CometChat.initiateCall`, `acceptCall`, `rejectCall`, `endCall`, `startCall`, `getActiveCall`, `clearActiveCall`, `addCallListener`, `CallListener`, `CallSettings` | `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `addIncomingCallListener`, the Push Notifications SDK calls module, `leave()` | -New in 5.1: `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `connect`, `getActiveSession`, `addIncomingCallListener` / `IncomingCallListener`, `addLoginListener` / `LoginListener`, `ParticipantEventListener.onParticipantConnected`, `CallAppSettingBuilder.overrideAdminHost / overrideClientHost / overrideCallsHost`. +New in 5.1: `CometChatCalls.initiateCall`, `endCall(sessionId, status)`, `connect`, `getActiveSession`, `addIncomingCallListener` / `IncomingCallListener`, `addLoginListener` / `LoginListener`, `ParticipantEventListener.onParticipantConnected`. The step-by-step move from Chat SDK signalling is on [Migrating from Legacy Ringing](/calls/android/migrating-from-legacy-ringing). diff --git a/calls/android/setup.mdx b/calls/android/setup.mdx index 7f08ea651..d60945a63 100644 --- a/calls/android/setup.mdx +++ b/calls/android/setup.mdx @@ -215,9 +215,6 @@ The `CallAppSettings` class configures the SDK initialization: |-----------|------|----------|-------------| | `appId` | String | Yes | Your CometChat App ID | | `region` | String | Yes | Your app region (`us` or `eu`) | -| `overrideAdminHost(host)` | String | No | Admin API host of a staging or dedicated deployment | -| `overrideClientHost(host)` | String | No | Client API host of that deployment; the same value the Chat SDK and the Push SDK get | -| `overrideCallsHost(host)` | String | No | Calls API host of that deployment, where call tokens are issued | @@ -273,37 +270,11 @@ CometChatCalls.init(this, callAppSettings, new CometChatCalls.CallbackListener - **Initialize once, with the final hosts.** The SDK keeps the hosts from its first `init()`; a later `init()` with - different hosts is ignored for the rest of the process. If you use the CometChat UI Kit with `enableCalling`, it - initializes the Calls SDK for you with the hosts from `UIKitSettings`, so do not call `init()` again yourself. + **Initialize once.** The SDK keeps the settings from its first `init()`; a later `init()` is ignored for the rest + of the process. If you use the CometChat UI Kit with `enableCalling`, it initializes the Calls SDK for you, so do + not call `init()` again yourself. -### Staging or dedicated deployments - -Pass the deployment's hosts with the override methods. All three are needed: the Push SDK and the Chat SDK use the -client host, and call tokens come from the calls host. - - - -```kotlin -val callAppSettings = CallAppSettings.CallAppSettingBuilder(appId, region) - .overrideAdminHost("$appId.api-$region.example.com/v3") - .overrideClientHost("$appId.apiclient-$region.example.com/v3") - .overrideCallsHost("$appId.call-$region.example.com/v3") - .build() -``` - - -```java -CallAppSettings callAppSettings = new CallAppSettings.CallAppSettingBuilder(appId, region) - .overrideAdminHost(appId + ".api-" + region + ".example.com/v3") - .overrideClientHost(appId + ".apiclient-" + region + ".example.com/v3") - .overrideCallsHost(appId + ".call-" + region + ".example.com/v3") - .build(); -``` - - - ## Check Initialization Status You can verify if the SDK has been initialized using the `isInitialized()` method: diff --git a/calls/android/troubleshooting.mdx b/calls/android/troubleshooting.mdx index 1dc1d7068..e98ddaae3 100644 --- a/calls/android/troubleshooting.mdx +++ b/calls/android/troubleshooting.mdx @@ -52,7 +52,6 @@ sdk_version: "5.x" | Call drops when app goes to background | No foreground service | Implement a foreground service to keep the call alive. See [Background Handling](/calls/android/background-handling) | | Incoming calls do not ring | Push Notifications SDK calls module missing, push not handed to `handlePushNotification`, or the device is not ring-ready | Add `push-notifications-calls-android`, pass every push with `sentTimeMs`, run `ensureRingReady`. See [Push Ringing](/calls/android/voip-calling) | | Rings, then the process dies before Answer | Call push sent at normal FCM priority (`BFGS denied`) | Send call pushes at high priority | -| No push on a staging or dedicated deployment | Push SDK registered the token on the default host | Pass the same `overrideClientHost` to `PNConfiguration` as to the Chat and Calls SDKs | | Caller never sees "Ringing…" / answered | Caller joined right after `initiateCall` | Stay on the prepared session and join on `onParticipantJoined`. See [Ringing](/calls/android/ringing) | | Audio call opens the camera | Session type not set at `initiateCall` | Pass `sessionSettings` with `setSessionType(SessionType.VOICE)` in `InitiateCallOptions` | | Ring keeps playing after logout | Only the Chat SDK was logged out | Call `CometChatCalls.logout` too (or `CometChatUIKit.logout`) | diff --git a/calls/android/voip-calling.mdx b/calls/android/voip-calling.mdx index 1a793ac5c..84ed699f9 100644 --- a/calls/android/voip-calling.mdx +++ b/calls/android/voip-calling.mdx @@ -90,7 +90,6 @@ A calls-only app can start from the settings it already has: `PNConfiguration.fr | `setDefaultRingTimeoutMs(ms)` | 45 000 | Ring window when the push carries no `ringTimeout`; 1 s to 10 min | | `setShowMissedCallNotification(bool)` | `true` | Post a missed-call notification | | `setRequestCallPermissions(bool)` | `true` | Ask for microphone (and camera) on Answer | -| `overrideClientHost(host)` | none | Same value as the Chat and Calls SDKs' `overrideClientHost`; see [Staging deployments](#staging-or-dedicated-deployments) | | `setInlineReplyEnabled(bool)` | `true` | Reply from a chat notification (needs the Chat SDK) | | `setTokenRegistrar(CHAT \| CALLS \| AUTO)`, `setAuthToken`, `setAutoRegisterToken` | `AUTO`, none, `true` | Token registration; leave the defaults | @@ -369,24 +368,13 @@ This is the case the push exists for. 1. The high-priority push starts the app process to run your Firebase service. 2. `handlePushNotification` finds no in-memory configuration and restores it from disk, then installs the calls module. -3. If the Calls SDK is not initialised yet the module initialises it with the saved hosts, and resolves the auth token (Chat SDK, then Calls SDK, then an explicit `setAuthToken`). +3. If the Calls SDK is not initialised yet the module initialises it, and resolves the auth token (Chat SDK, then Calls SDK, then an explicit `setAuthToken`). 4. The ring runs on a foreground service of type `phoneCall`, so the system keeps the process alive through the answer. An alarm backs up the ring timeout. 5. On Answer, the trampoline activity asks for permissions and starts your answer activity, which joins. 6. A decline or timeout is sent through the WorkManager queue, so it goes out even if the process dies right after the tap. Initialise the Calls SDK (and your own SDK setup) in `Application.onCreate` from stored credentials, not only after a screen opens, so the answer activity finds a ready SDK. -## Staging or dedicated deployments - -If your Chat and Calls SDKs use `overrideClientHost`, give the Push SDK the same host. Otherwise the token is registered on the default production host, where your app id does not exist, and no push ever arrives. - -```kotlin -PNConfiguration.Builder(APP_ID, REGION) - .overrideClientHost("$APP_ID.apiclient-us.cometchat-staging.com/v3.0") -``` - -The host is saved with the configuration, so a token that rotates while the app is closed still registers on the right host, and the Calls SDK is initialised with the matching hosts on a push wake. - ## Tokens You do not register the push token. The SDK fetches it from Firebase and registers it on login, under the Chat auth token when there is one and the Calls auth token otherwise; it unregisters on logout. `registerToken()` / `unregisterToken()` exist for apps that set `setAutoRegisterToken(false)`. @@ -553,11 +541,10 @@ Ids are keys: the same id registered twice replaces the listener. Remove it when | Symptom | Check | |---------|-------| -| No push arrives | Package name matches the Firebase app; `google-services.json` present; provider id set; the user is logged in (the token registers on login); `overrideClientHost` matches the Chat and Calls SDKs | +| No push arrives | Package name matches the Firebase app; `google-services.json` present; provider id set; the user is logged in (the token registers on login) | | Push arrives, nothing rings | `handlePushNotification` is called for every push with `sentTimeMs`; `ensureRingReady` is `READY`; the push was not older than its ring window | | Rings but no full-screen screen on a locked phone | `FSI_REVOKED` on Android 14+: send the user to the settings page from `settingsIntentFor` | | Rings, then the process dies before Answer | Call pushes sent at normal priority (`BFGS denied`); send at high priority | -| Answer opens the app but joins fail with "app id does not exist" | Staging or dedicated host not given to the Push SDK with `overrideClientHost` | | Caller never sees "Ringing…" | The caller joined right after `initiateCall` instead of listening on the prepared session | | Ring keeps playing after the user logged out | The app logged out of the Chat SDK only; also log out of the Calls SDK | | Tapping a missed call does nothing | Set `setMissedCallActivity` | diff --git a/notifications/android-push-notifications.mdx b/notifications/android-push-notifications.mdx index a38452a94..8f29c2cab 100644 --- a/notifications/android-push-notifications.mdx +++ b/notifications/android-push-notifications.mdx @@ -141,10 +141,6 @@ val config = PNConfiguration.Builder(APP_ID, REGION) .setNotificationSmallIcon(R.drawable.ic_notification) .setInlineReplyEnabled(true) // default true; needs the Chat SDK .setAnswerActivity(CometChatOngoingCallActivity::class.java) // calls: the screen opened on Answer - .apply { - // Staging or dedicated deployment: same host as the Chat and Calls SDKs - CometChatUIKit.getAuthSettings()?.overrideClientHost?.let { overrideClientHost(it) } - } .build() CometChatPushNotifications.init(this, config) @@ -159,7 +155,6 @@ CometChatPushNotifications.init(this, config) | `setIncomingCallActivity(Class?)` | the SDK's screen | Your own incoming-call screen | | `setMissedCallActivity(Class?)` | none | Opened when a missed-call notification is tapped | | `setDefaultRingTimeoutMs(ms)`, `setShowMissedCallNotification(bool)`, `setRequestCallPermissions(bool)` | 45 000, `true`, `true` | Ring behaviour; see [Push Ringing](/calls/android/voip-calling) | -| `overrideClientHost(host)` | none | Same value as the Chat and Calls SDKs' `overrideClientHost` | | `setTokenRegistrar(CHAT | CALLS | AUTO)`, `setAuthToken`, `setAutoRegisterToken` | `AUTO`, none, `true` | Which login's auth token registers the device; leave the defaults | Deep links from chat notifications: @@ -344,7 +339,6 @@ Everything about it, including your own ring screen, theming and what happens wh | Symptom | Quick checks | | --- | --- | | No notifications | Package name matches the Firebase app, `google-services.json` present, notification permission granted, Provider ID correct and Push Notifications enabled in the dashboard, user logged in (the token registers on login) | -| No pushes on a staging or dedicated deployment | `overrideClientHost` on `PNConfiguration` matches the Chat and Calls SDKs | | Pushes stop after a while | `onNewToken` not forwarded to `handleTokenRefresh` | | Chat notification tap does nothing | `setOnNotificationTapListener` not set, or the activity's launch mode drops extras | | Calls do not ring | `push-notifications-calls-android` missing, `setAnswerActivity` not set, push sent at normal priority, or `ensureRingReady` not `READY` | diff --git a/ui-kit/android/calling-integration.mdx b/ui-kit/android/calling-integration.mdx index b56caf363..ef70d6abf 100644 --- a/ui-kit/android/calling-integration.mdx +++ b/ui-kit/android/calling-integration.mdx @@ -108,20 +108,6 @@ Run this in `Application.onCreate` from stored credentials, not only after a scr **Enabling calling in an app that already has logged-in users requires a re-login.** `CometChatUIKit.login()` short-circuits when the same uid already has a persisted session, so the Calls SDK never logs in: the Calls SDK has no auth token and `CometChatCallLogs` shows a silent error state (logcat: `User auth token cannot be null`). After calling is first enabled, log out and log in again (`CometChatUIKit.logout(...)` then `CometChatUIKit.login(...)`), and wire `onError` on `CometChatCallLogs` during development so this failure is visible. -### Staging or dedicated deployments - -Pass all three hosts; the UI Kit forwards them to the Chat SDK and the Calls SDK. The Push SDK gets the client host in the next step. - -```kotlin -UIKitSettings.UIKitSettingsBuilder() - .setAppId(APP_ID).setRegion(REGION).setAuthKey(AUTH_KEY) - .overrideAdminHost("$APP_ID.api-$REGION.example.com/v3") - .overrideClientHost("$APP_ID.apiclient-$REGION.example.com/v3") - .overrideCallsHost("$APP_ID.call-$REGION.example.com/v3") - .setEnableCalling(true) - .build() -``` - ## Step 3: Configure push ringing Initialise the Push Notifications SDK once, after the UI Kit (the FCM provider id and the answer activity are all it needs). `CometChatOngoingCallActivity` is the UI Kit's in-call screen; registered as the answer activity it reads the answered call from its intent and joins. @@ -136,10 +122,6 @@ private fun initPushNotifications() { .setProviderId(PROVIDER_ID) // FCM provider id from the dashboard .setAnswerActivity(CometChatOngoingCallActivity::class.java) // the UI Kit's call screen .setNotificationSmallIcon(R.drawable.ic_notification) - .apply { - // Same host as the Chat and Calls SDKs, or the token registers on the wrong app - CometChatUIKit.getAuthSettings()?.overrideClientHost?.let { overrideClientHost(it) } - } .build() CometChatPushNotifications.init(this, config) } @@ -154,10 +136,6 @@ private fun initPushNotifications() { .setProviderId(PROVIDER_ID) // FCM provider id from the dashboard .setAnswerActivity(CometChatOngoingCallActivity::class.java) // the UI Kit's call screen .setNotificationSmallIcon(R.drawable.ic_notification) - .apply { - // Same host as the Chat and Calls SDKs, or the token registers on the wrong app - CometChatUIKit.getAuthSettings()?.overrideClientHost?.let { overrideClientHost(it) } - } .build() CometChatPushNotifications.init(this, config) } diff --git a/ui-kit/android/methods.mdx b/ui-kit/android/methods.mdx index f3acb689b..377311b1d 100644 --- a/ui-kit/android/methods.mdx +++ b/ui-kit/android/methods.mdx @@ -75,7 +75,6 @@ The `UIKitSettings` is an important parameter of the `init()` function. It serve | **setExtensions** | `List` | Sets the list of extension that need to be added in UI Kit | | **dateTimeFormatterCallback** | `DateTimeFormatterCallback` | Interface containing callback methods to format different types of timestamps. | | **setEnableCalling** | `Boolean` | Turns on calling: the UI Kit initialises the Calls SDK during `init()` and logs the user into it during `login()`. Default `false`. See [Calling Integration](/ui-kit/android/calling-integration) | -| **overrideAdminHost / overrideClientHost / overrideCallsHost** | `String` | Hosts of a staging or dedicated deployment, forwarded to the Chat SDK and the Calls SDK. Give the Push Notifications SDK the same client host | | **setCallSettingsBuilder** | `CometChatCalls.SessionSettingsBuilder` | Default session settings for the call screens | | **setEnableThreadSubscription** | `Boolean` | *Deprecated in v6.1.0.* Global switch for the thread subscription feature. Default `true` from v6.1.0 (`false` on v6.0.x, where it is the opt-in). Kept as the only app-wide opt-out — pass `false` to turn the feature off everywhere. To hide a single surface, prefer the per-screen visibility methods. See [Thread Subscription](/ui-kit/android/guide-threaded-messages#thread-subscription) | diff --git a/ui-kit/android/troubleshooting.mdx b/ui-kit/android/troubleshooting.mdx index dd4aadf7c..c0e5468f6 100644 --- a/ui-kit/android/troubleshooting.mdx +++ b/ui-kit/android/troubleshooting.mdx @@ -83,7 +83,6 @@ description: "Common failure modes and fixes for the CometChat Android V6 UI Kit | Incoming calls never ring | Push ringing not configured, or the push not handed to the SDK | `CometChatPushNotifications.init` with `setProviderId` + `setAnswerActivity(CometChatOngoingCallActivity::class.java)`, and `handlePushNotification(this, data, sentTime)` for every push in your `FirebaseMessagingService`. See [Push Ringing](/calls/android/voip-calling) | | Rings on an emulator or a fresh install, but not on a user's phone | Device settings silence the ring | Run `CometChatPushNotifications.ensureRingReady(activity)` after sign-in and show any result other than `READY` (notifications off, muted channel, full-screen intent revoked, Do Not Disturb) | | Rings, then the process dies before Answer | Call push sent at normal FCM priority | Send call pushes at high priority | -| No push on a staging or dedicated deployment | Push token registered on the default host | Pass the same `overrideClientHost` to `PNConfiguration` as to `UIKitSettings` | | Answer opens the app but the call does not join | Answer activity not the UI Kit's, or SDK not initialised on a cold start | Register `CometChatOngoingCallActivity` as the answer activity and run `CometChatUIKit.init` in `Application.onCreate` from stored credentials | | Caller's screen stays on "calling…" and never shows "ringing…" | Callee's device not reached (push not delivered) | Check the callee's `ensureRingReady`, provider id and host; the caller times out as unanswered after 45 s | | Audio call opens the caller's camera | Custom call start without a session type | Place calls with `CallInitiationUtils.initiateUserCall`, or set `setSessionType(SessionType.VOICE)` in the `InitiateCallOptions` session settings | From dfb04e11afad2eccd001ca8127f8872650f6b458 Mon Sep 17 00:00:00 2001 From: Hritika Date: Thu, 15 Oct 2026 17:54:51 +0530 Subject: [PATCH 6/7] docs(android): UI Kit answer activity matches master-app; trim Push SDK internals The UI Kit has no Push SDK dependency, so the answer activity is a small app class that builds the call with CallRingingUtils.buildIncomingCall and opens CometChatOngoingCallActivity. Fix incoming-call to use real APIs. Remove MANUAL mode, token registrar options, diagnostics, decline-queue and identifier details. Co-Authored-By: Claude Opus 5.5 --- .../android/migrating-from-legacy-ringing.mdx | 8 +- calls/android/ringing.mdx | 4 +- calls/android/voip-calling.mdx | 139 ++---------------- notifications/android-push-notifications.mdx | 23 +-- ui-kit/android/call-features.mdx | 6 +- ui-kit/android/calling-integration.mdx | 100 ++++++++++--- ui-kit/android/incoming-call.mdx | 108 +++++--------- ui-kit/android/llms-android-v6.mdx | 4 +- ui-kit/android/ongoing-call.mdx | 30 ++-- ui-kit/android/outgoing-call.mdx | 2 +- ui-kit/android/troubleshooting.mdx | 4 +- ui-kit/android/upgrading-from-v5.mdx | 2 +- 12 files changed, 175 insertions(+), 255 deletions(-) diff --git a/calls/android/migrating-from-legacy-ringing.mdx b/calls/android/migrating-from-legacy-ringing.mdx index 62f4e6000..5fac668ed 100644 --- a/calls/android/migrating-from-legacy-ringing.mdx +++ b/calls/android/migrating-from-legacy-ringing.mdx @@ -81,9 +81,7 @@ dependencies { - `minSdk` 26 or higher. - Keep your Firebase Messaging dependency and `google-services.json`; the Push SDK never compiles against Firebase. - -`setCallMode` defaults to `AUTO`, which is what this page assumes. Adding the `push-notifications-calls-android` artifact is the opt-in: core detects it and installs the ringer. There is no second initialiser. - +Adding the `push-notifications-calls-android` artifact turns ringing on. There is no second initialiser. ## Step 2: Replace the push configuration @@ -161,8 +159,6 @@ The Push SDK registers the token on login and unregisters it on logout by itself - `FirebaseMessaging.getInstance().token` + `registerToken(...)` / `CometChatNotifications.registerPushToken(...)` after login - `unregisterToken(...)` / `unregisterPushToken(...)` before logout -Call `registerToken()` yourself only if you set `setAutoRegisterToken(false)`. - ## Step 5: Delete the app-side ringing Everything in this list is now done by the Push SDK. Remove it, or it will fight the SDK's ring (two ringtones, two screens, a busy reject sent on top of the SDK's). @@ -285,7 +281,7 @@ CometChatUIKit.logout(callback) // instead of CometChat.logout: logs out of Ch Keep it, but drive it from the Push SDK instead of the `CallListener`: -1. Register it with `PNConfiguration.Builder.setIncomingCallActivity(YourRingActivity::class.java)`. The Push SDK still rings, pre-warms the session, times out and sends the decline; only the screen is yours. +1. Register it with `PNConfiguration.Builder.setIncomingCallActivity(YourRingActivity::class.java)`. The Push SDK still rings, times out and sends the decline; only the screen is yours. 2. In `onCreate`, read `CallBundles.getCall(intent)` and call `CometChatCallNotifications.onIncomingCallScreenOpened(sessionId) { reason -> finish() }` so the SDK closes the screen when the call ends elsewhere. 3. Answer with `CometChatCallNotifications.answer(context, sessionId)` after asking for the microphone (and camera) yourself; `answer()` skips the SDK's own prompt. 4. Decline with `CometChatCallNotifications.decline(context, sessionId)`. diff --git a/calls/android/ringing.mdx b/calls/android/ringing.mdx index 472376a62..9983fc1d2 100644 --- a/calls/android/ringing.mdx +++ b/calls/android/ringing.mdx @@ -217,7 +217,7 @@ void listenOnPreparedSession(InitiatedCall call) { | Event | On the prepared session it means | |-------|----------------------------------| -| `onParticipantConnected(callee)` | The callee's phone got the push and the Push SDK pre-warmed the session: it is ringing there | +| `onParticipantConnected(callee)` | The callee's phone got the push: it is ringing there | | `onParticipantJoined(callee)` | The callee answered. Call `joinSession` now | | `onConnectionClosed` / `onSessionTimedOut` | The call ended before anyone joined (declined, cancelled from another device, or the session died) | @@ -384,7 +384,7 @@ See [Join Session](/calls/android/join-session) for the container, [Session Sett ## Decline a call -Declining is handled by the Push SDK: the Decline button on the notification and on its incoming-call screen sends `endCall(sessionId, "rejected")` through a queue that survives the process dying, and sends `"busy"` instead when the user is already in another call. The caller receives `onIncomingCallEnded` with that reason. +Declining is handled by the Push SDK: the Decline button on the notification and on its incoming-call screen sends `endCall(sessionId, "rejected")`, and sends `"busy"` instead when the user is already in another call. The caller receives `onIncomingCallEnded` with that reason. You only write decline code when you draw your own incoming-call screen: diff --git a/calls/android/voip-calling.mdx b/calls/android/voip-calling.mdx index 84ed699f9..8d5f5695d 100644 --- a/calls/android/voip-calling.mdx +++ b/calls/android/voip-calling.mdx @@ -2,7 +2,7 @@ title: "Push Ringing" sidebarTitle: "Push Ringing (VoIP)" sdk_version: "5.x" -description: "Ring incoming CometChat calls on Android with the Push Notifications SDK 2.0 calls module: FCM setup, configuration, the answer activity, permissions, busy and missed calls, your own ring screen, and behaviour when the app is killed." +description: "Ring incoming CometChat calls on Android with the Push Notifications SDK 2.0 calls module: FCM setup, configuration, the answer activity, permissions, missed calls and your own ring screen." --- The Push Notifications SDK's calls module turns a CometChat call push into a ringing phone: a call-style notification with Answer and Decline, a full-screen screen over the lock screen, the ringtone, a ring timeout, missed-call notifications and busy handling. It works with the app in the foreground, in the background or not running at all, and needs no Telecom `ConnectionService`. @@ -19,7 +19,7 @@ flowchart LR D -->|another call ringing| B[Declined as busy] R -->|Answer| P[Mic / camera prompt] P --> A[Your answer activity → joinSession] - R -->|Decline| Q["endCall(rejected) via durable queue"] + R -->|Decline| Q["endCall(rejected)"] R -->|45 s| T["endCall(unanswered) + missed call"] ``` @@ -42,7 +42,7 @@ dependencies { } ``` -Adding the calls artifact is the whole opt-in: core detects it and installs the ringer. The Chat SDK is optional for both artifacts; a calls-only app works. +Adding the calls artifact turns ringing on. The Chat SDK is optional; a calls-only app works. ## Step 2: Initialise once @@ -77,13 +77,10 @@ CometChatPushNotifications.init( -A calls-only app can start from the settings it already has: `PNConfiguration.from(callAppSettings)` in Kotlin (an extension in the calls module: `import com.cometchat.pushnotification.calls.from`), `CometChatCallPushConfig.from(callAppSettings)` in Java. - | Setter | Default | What it does | |--------|---------|--------------| | `setProviderId(id)` | none | The dashboard FCM provider the token is registered under | | `setAnswerActivity(Class)` | none, **required** | Started when the user answers, with the call in its extras | -| `setCallMode(CallMode.AUTO \| MANUAL)` | `AUTO` | `AUTO` rings. `MANUAL` only parses and forwards; see [MANUAL mode](#manual-mode) | | `setIncomingCallActivity(Class?)` | the SDK's own screen | Your full-screen ring screen; see [Your own incoming-call screen](#your-own-incoming-call-screen) | | `setMissedCallActivity(Class?)` | none | Opened when a missed-call notification is tapped. Without it an individual missed-call notification has no tap action | | `setNotificationSmallIcon(res)` | none | Status-bar icon for every notification | @@ -91,9 +88,6 @@ A calls-only app can start from the settings it already has: `PNConfiguration.fr | `setShowMissedCallNotification(bool)` | `true` | Post a missed-call notification | | `setRequestCallPermissions(bool)` | `true` | Ask for microphone (and camera) on Answer | | `setInlineReplyEnabled(bool)` | `true` | Reply from a chat notification (needs the Chat SDK) | -| `setTokenRegistrar(CHAT \| CALLS \| AUTO)`, `setAuthToken`, `setAutoRegisterToken` | `AUTO`, none, `true` | Token registration; leave the defaults | - -`build()` throws if the calls module is present, the mode is `AUTO`, and no answer activity is set. ## Step 3: Delegate from your Firebase service @@ -157,7 +151,7 @@ public class MyFirebaseService extends FirebaseMessagingService { ## Step 4: Join from the answer activity -On **Answer** the SDK stops the ring, asks for the microphone (and the camera for a video call), dismisses the keyguard and starts your answer activity with the call in its extras. Read it and join; there is nothing to "accept". +On **Answer** the SDK stops the ring, asks for the microphone (and the camera for a video call) and starts your answer activity with the call in its extras. Read it and join; there is nothing to "accept". @@ -232,7 +226,7 @@ public class CallScreenActivity extends AppCompatActivity { -`IncomingCall` fields: `sessionId`, `callerUid`, `callerName`, `callerAvatarUrl`, `callType`, `receiverType`, `receiverUid`, `conversationId`, `initiatedAtMs`, `ringTimeoutMs`, `source` (`PUSH`; `WEBSOCKET` is reserved for web), `extras`, plus the derived `isVideo`, `isPeerCall`, `initials` and `expiresAtMs`. The ring window is anchored to the caller's `initiatedAtMs`, not to a timer on this device, so both sides stop ringing together however late the push arrived; prefer `expiresAtMs` to a timer of your own. +`IncomingCall` gives you `sessionId`, `callerUid`, `callerName`, `callerAvatarUrl`, `isVideo`, `receiverType` and `receiverUid`. Declare the activity so it can show over the lock screen: @@ -246,7 +240,7 @@ Declare the activity so it can show over the lock screen: ``` -The notification stays as a silent "Connecting call…" entry until the Calls SDK reports the session joined (60 s at most). Once joined, any in-call notification is yours to show; the SDK posts none. For a foreground service during the call see [Background Handling](/calls/android/background-handling). +The SDK posts no notification during the call. For a foreground service while the call runs, see [Background Handling](/calls/android/background-handling). ## Step 5: Permissions and ring readiness @@ -259,9 +253,9 @@ Declare in the manifest what the SDKs only request: ``` -Everything else (full-screen intent, foreground service of type `phoneCall`, wake lock, vibrate, the ring activity, the answer trampoline and the ring service) comes from the calls module's own manifest. No `MANAGE_OWN_CALLS`, `READ_PHONE_STATE` or `ANSWER_PHONE_CALLS`. +Everything else comes from the calls module's own manifest. -**Microphone and camera.** The SDK asks at the moment of answering: the answer is recorded first so the ring stops, the system prompt appears, and your answer activity opens whatever the user chose. A denied microphone joins muted and never fails the call. Ask earlier as well (at sign-in, for example) so the first call is not where the user meets the prompt, and give your call screen a way to the app's settings page for a permission denied for good. If your app prompts itself, turn the SDK prompt off with `setRequestCallPermissions(false)`. +**Microphone and camera.** The SDK asks when the user answers. A denied microphone joins muted and does not fail the call. Ask earlier as well (at sign-in, for example) so the first call is not where the user meets the prompt, and give your call screen a way to the app's settings page for a permission denied for good. If your app prompts itself, turn the SDK prompt off with `setRequestCallPermissions(false)`. **Ring readiness.** Several device settings silence a call with no error anywhere. Check once after sign-in and tell the user: @@ -302,88 +296,27 @@ CometChatPushNotifications.ensureRingReady(this, result -> { Call `ensureRingReady` only after any other permission request of yours has returned; Android drops a request made while another dialog is open. -**Diagnostics for a bug report.** `getDiagnostics(context)` returns a redacted snapshot (whether a provider id exists, never what it is; never a token or payload): - -| Key | Read it as | -|-----|------------| -| `hasPersistedConfig` | `false`: `init` never ran, or ran with a different app id | -| `callsModulePresent` | `false`: the calls artifact is not on the classpath, nothing will ring | -| `answerActivityResolves` | `false`: the configured answer activity cannot be found | -| `hasProviderId`, `firebasePresent` | `false`: token registration cannot complete | -| `canUseFullScreenIntent` | `false` on Android 14+: no ring screen on a locked device | -| `incomingChannelImportance` | below `IMPORTANCE_HIGH`: no heads-up and no sound | -| `interruptionFilter` | anything but `INTERRUPTION_FILTER_ALL`: Do Not Disturb is filtering | -| `chatSdkPresent`, `chatLoggedIn`, `callMode`, `sdkInt`, `manufacturer`, `model` | Which auth token registration can use; `MANUAL` explains a phone that never rings by design; OEM background restrictions | - -`verifyPushSetup(callback)` registers the token and reports `OK`, `TOKEN_NOT_REGISTERED`, `NOT_LOGGED_IN`, `TEST_PUSH_UNAVAILABLE` (registration worked, the loopback test push is not available on the backend yet: a warning, not a failure) or `TEST_PUSH_TIMEOUT` (the test push never arrived: delivery is broken). - -Anything that takes a `PNCallback` can fail with `NOT_INITIALISED`, `NOT_LOGGED_IN`, `PROVIDER_REQUIRED`, `FCM_UNAVAILABLE`, `SERVER_ERROR` or `NETWORK_ERROR`. The token value is never logged, only its length and a six-character prefix. - ## How a ring ends -Every way out is handled by the SDK and needs no code in your app. - -| What happened | On the callee's device | What the caller learns | -|---------------|------------------------|------------------------| -| The user answers | Ring stops, "Connecting call…" until your screen joins | `onParticipantJoined` | -| The user declines | Ring stops; `endCall(sessionId, "rejected")` is queued and retried across process death | `onIncomingCallEnded(call, "rejected")` | -| The user declines while in another call | Same, with `"busy"` | `"busy"` | -| The caller cancels | Ring stops on the `cancelled` push, or sooner when the prepared session closes; missed-call notification | Nothing; the caller ended it | -| Nobody answers a 1:1 call | At the ring window (45 s by default, anchored to the caller's `initiatedAt`) the ring stops, a missed call is posted and `endCall(sessionId, "unanswered")` is sent | `"unanswered"` | -| Answered on another device | Ring stops on the `ongoing` push, no missed call | `onParticipantJoined` | -| Nobody answers a group call | Ring stops, missed call posted, **nothing sent**; the call keeps running | Nothing | -| The user declines a group call | Ring stops here only; the prepared session is left, nothing sent | Nothing | -| The user logs out while ringing | Ring stops, nothing sent, no missed call | Rings to its own window, then `"unanswered"` | - -On a decline or a 1:1 timeout the SDK does not leave the prepared session first: `endCall` announces the status to the room while still in it, then leaves, so the caller hears it at once. Once the session is prepared, the Calls SDK reporting it ended also stops the ring immediately, without waiting for a terminal push. - -Declines and `unanswered` go through a WorkManager queue (`cometchat-pn-decline-`): network required, exponential backoff from 10 s, given up after 5 attempts, and a call the server already ended (`ERR_CALL_TERMINATED`) counts as delivered. +The SDK handles every way a ring ends; you write no code for it. A decline sends `rejected` to the caller (`busy` if the user is in another call). A 1:1 call nobody answers stops after the ring window (45 s by default), posts a missed call and sends `unanswered`. A cancel by the caller, or an answer on another device, stops the ring here. -## A call while in a call - -Call waiting, the way the phone app does it. "In a call" means the Calls SDK is joined to a session, or a call answered here is still joining; a session that is only prepared while it rings does not count. - -| Situation | Behaviour | -|-----------|-----------| -| Call while in a call | Rings over the live call with the normal ring; the text adds "Answering ends your current call". The live call is untouched | -| Answer (End & accept) | Leaves the live call, waits for the Calls SDK to release it, prepares the new session, opens the answer activity (`onNewIntent` if it is still up) | -| Decline | `busy` to the caller; the live call continues | -| No action | `unanswered` to the caller, missed call; the live call continues | -| Call while another one is already ringing here | Declined as `busy` without ringing, missed call. The first keeps ringing | -| Call while the user's own outgoing call is ringing | Declined as `busy` without ringing, missed call | +A call that arrives during another call rings over it with "Answering ends your current call". Answering leaves the live call and joins the new one; declining sends `busy` and the live call continues. ## Missed calls -Posted on channel `missed_calls_v1` as "Missed call" / "Missed group call" with the caller's name and call type, one per session, grouped under the SDK's own summary ("N missed calls") so Android never folds them into your other notifications. The summary is rebuilt from the tray, so it survives the process dying. - -Posted when: nobody answers in the ring window; a `cancelled`, `unanswered` or `timed_out` push ends a ring here; such a push arrives for a call that never rang here (the `initiated` push was lost or overtaken); an `initiated` push arrives already expired; a call is declined as busy without ringing. - -Not posted when: answered here or on another device; declined (`rejected` or `busy`) here; ended; the user's own outgoing call; logout while ringing. - -Tapping the summary opens the launcher. Tapping an individual missed call opens the activity from `setMissedCallActivity` with the call in its extras; **without that setting it does nothing**. Turn missed calls off with `setShowMissedCallNotification(false)`. This is a notification only; call history comes from [Call Logs](/calls/android/call-logs). +The SDK posts a missed-call notification when a call is not answered here. Tapping it opens the activity from `setMissedCallActivity` with the call in its extras; **without that setting the tap does nothing**. Turn them off with `setShowMissedCallNotification(false)`. Call history comes from [Call Logs](/calls/android/call-logs). ## When the app is killed -This is the case the push exists for. - -1. The high-priority push starts the app process to run your Firebase service. -2. `handlePushNotification` finds no in-memory configuration and restores it from disk, then installs the calls module. -3. If the Calls SDK is not initialised yet the module initialises it, and resolves the auth token (Chat SDK, then Calls SDK, then an explicit `setAuthToken`). -4. The ring runs on a foreground service of type `phoneCall`, so the system keeps the process alive through the answer. An alarm backs up the ring timeout. -5. On Answer, the trampoline activity asks for permissions and starts your answer activity, which joins. -6. A decline or timeout is sent through the WorkManager queue, so it goes out even if the process dies right after the tap. - -Initialise the Calls SDK (and your own SDK setup) in `Application.onCreate` from stored credentials, not only after a screen opens, so the answer activity finds a ready SDK. - -## Tokens +The high-priority push starts the app process and the SDK rings from its saved configuration. Initialise the Calls SDK (and your own SDK setup) in `Application.onCreate` from stored credentials, not only after a screen opens, so the answer activity finds a ready SDK. -You do not register the push token. The SDK fetches it from Firebase and registers it on login, under the Chat auth token when there is one and the Calls auth token otherwise; it unregisters on logout. `registerToken()` / `unregisterToken()` exist for apps that set `setAutoRegisterToken(false)`. +## Tokens and logout -Log out of the Calls SDK when the user signs out (`CometChatCalls.logout`, or `CometChatUIKit.logout` with the UI Kit). That is what stops a ring still playing for the old user and unregisters the token; `CometChat.logout` alone does neither. +You do not register the push token: the SDK registers it on login and unregisters it on logout. Log out of the Calls SDK when the user signs out (`CometChatCalls.logout`, or `CometChatUIKit.logout` with the UI Kit). That stops a ring still playing for the old user and unregisters the token; `CometChat.logout` alone does neither. ## Your own incoming-call screen -Set `setIncomingCallActivity(...)`. The SDK still owns the ringtone, the pre-warm, the timeout, the keep-alive and the decline queue; only the full-screen UI is yours. +Set `setIncomingCallActivity(...)`. The SDK still plays the ringtone and handles the timeout and the decline; only the full-screen UI is yours. @@ -456,14 +389,6 @@ public class MyRingActivity extends AppCompatActivity { -If the same activity is also your answer activity (one screen that rings, then becomes the call), the ring ends with `RING_END_ANSWERED` a moment before the answer intent reaches it through `onNewIntent`. Use the overload that passes the reason and finish only on other reasons: - -```kotlin -CometChatCallNotifications.onIncomingCallScreenOpened(call.sessionId) { reason -> - if (reason != CometChatCallNotifications.RING_END_ANSWERED) finish() -} -``` - Without `onIncomingCallScreenOpened`, a cancelled call leaves your screen on the lock screen showing an Answer button for a call that no longer exists. ## Theming the SDK's incoming-call screen @@ -486,35 +411,12 @@ The SDK's screen and notifications read their look from resources. Override any #F44649 ``` -Also overridable: - -- **Strings**: `cometchat_pn_incoming_call_text`, `cometchat_pn_incoming_group_call_text`, `cometchat_pn_action_answer`, `cometchat_pn_action_decline`, `cometchat_pn_call_type_audio`, `cometchat_pn_call_type_video`, `cometchat_pn_connecting_call_text`, `cometchat_pn_call_waiting_text`, `cometchat_pn_missed_call_title`, `cometchat_pn_missed_group_call_title`, `cometchat_pn_missed_call_text`, `cometchat_pn_missed_calls_summary`, and the channel names and descriptions `cometchat_pn_channel_{incoming,ongoing,missed}_{name,description}`. -- **Drawables**: `cometchat_pn_ic_call`, `cometchat_pn_ic_call_end`, `cometchat_pn_ic_notification_call`, `cometchat_pn_ic_notification_call_missed`, `cometchat_pn_bg_button_accept`, `cometchat_pn_bg_button_decline`, `cometchat_pn_bg_avatar_circle`. -- **Theme**: `CometChatPNIncomingCallTheme`. -- **Layout**: `cometchat_pn_activity_incoming_call`, which must keep the view ids `cometchat_pn_initials`, `cometchat_pn_caller_name`, `cometchat_pn_call_type`, `cometchat_pn_decline` and `cometchat_pn_answer`. - -The avatar is always the caller's initials; the screen does not load the avatar URL. The ringtone is the device's default ringtone on channel `incoming_calls_v1`; because Android freezes a channel's sound at creation, the SDK never changes it afterwards. +The strings, drawables and layout of the screen (all prefixed `cometchat_pn_`) can be overridden the same way. If you use the CometChat UI Kit, it already overrides the six colours with its own light and dark palette. Your app's override still wins over the UI Kit's. -## MANUAL mode - -`setCallMode(CallMode.MANUAL)` makes the SDK parse, gate and forward only. It never rings and never touches the Calls SDK on its own. The call reaches `CometChatCalls.IncomingCallListener.onIncomingCallReceived`, and from there every step is yours: - -```kotlin -CometChatCallNotifications.Calls.showIncomingNotification(context, call) // post the ring, arm expiry -CometChatCallNotifications.Calls.verify(context, call) // pre-warm connect() -CometChatCallNotifications.Calls.keepAlive(context, call) // foreground service while ringing -CometChatCallNotifications.Calls.stopRinging(context, sessionId) -CometChatCallNotifications.Calls.showMissedCall(context, call) -``` - - -In MANUAL mode with nothing built, the call is silently lost: no notification, no ringtone, no screen, nothing sent to the caller, who rings out to `unanswered`. Use MANUAL only if you are rebuilding the whole ring. - - ## Knowing about a call in your own code Listen on the Calls SDK, not on this SDK. It is the same callback whether the call arrived by push on a killed process or on a live one: @@ -528,15 +430,6 @@ CometChatCalls.addIncomingCallListener("my-screen", object : CometChatCalls.Inco Ids are keys: the same id registered twice replaces the listener. Remove it when the screen goes away. -## Push SDK identifiers - -| Kind | Value | -|------|-------| -| Notification channels | `incoming_calls_v1` (high, ringtone, vibration), `ongoing_calls_v1` (low, "Connecting call…"), `missed_calls_v1` (default) | -| Statuses the SDK sends with `endCall` | `rejected`, `busy`, `unanswered` | -| Intent extras | `com.cometchat.pushnotification.calls.call.`; read with `CallBundles.getCall` | -| Timings | ring window 45 s default, answer connecting cap 60 s, decline queue 5 attempts | - ## Troubleshooting | Symptom | Check | diff --git a/notifications/android-push-notifications.mdx b/notifications/android-push-notifications.mdx index 8f29c2cab..47d8412ac 100644 --- a/notifications/android-push-notifications.mdx +++ b/notifications/android-push-notifications.mdx @@ -124,7 +124,7 @@ dependencies { ``` -Everything else the ringer needs (full-screen intent, the `phoneCall` foreground service, wake lock, vibrate, the incoming-call activity, the answer trampoline, the ring service and the inline-reply receiver) is declared by the SDK and merges in. There is no `ConnectionService`, no `MANAGE_OWN_CALLS`, `READ_PHONE_STATE` or `ANSWER_PHONE_CALLS`. +Everything else the ringer needs is declared by the SDK and merges in. ## 4. Initialise the SDK @@ -133,14 +133,12 @@ Once per install, after the UI Kit (or the Chat and Calls SDKs) is initialised. ```kotlin lines import com.cometchat.pushnotification.CometChatPushNotifications import com.cometchat.pushnotification.PNConfiguration -import com.cometchat.uikit.kotlin.presentation.ongoingcall.ui.CometChatOngoingCallActivity -// Compose: com.cometchat.uikit.compose.presentation.ongoingcall.ui.CometChatOngoingCallActivity val config = PNConfiguration.Builder(APP_ID, REGION) .setProviderId(PROVIDER_ID) // the FCM provider you created .setNotificationSmallIcon(R.drawable.ic_notification) .setInlineReplyEnabled(true) // default true; needs the Chat SDK - .setAnswerActivity(CometChatOngoingCallActivity::class.java) // calls: the screen opened on Answer + .setAnswerActivity(CallAnswerActivity::class.java) // calls: the screen opened on Answer .build() CometChatPushNotifications.init(this, config) @@ -148,14 +146,13 @@ CometChatPushNotifications.init(this, config) | Setter | Default | What it does | | --- | --- | --- | -| `setProviderId(id)` | none | Provider the device token is registered under. Registration fails with `PROVIDER_REQUIRED` without it | +| `setProviderId(id)` | none | Provider the device token is registered under | | `setNotificationSmallIcon(res)` | none | Status-bar icon for every notification the SDK posts | | `setInlineReplyEnabled(bool)` | `true` | Reply action on chat notifications | -| `setAnswerActivity(Class)` | none; required with the calls module | Opened when the user answers a call. UI Kit apps register `CometChatOngoingCallActivity` | +| `setAnswerActivity(Class)` | none; required with the calls module | Opened when the user answers a call. UI Kit apps use a small activity that opens `CometChatOngoingCallActivity`; see [Calling Integration](/ui-kit/android/calling-integration#step-3-configure-push-ringing) | | `setIncomingCallActivity(Class?)` | the SDK's screen | Your own incoming-call screen | | `setMissedCallActivity(Class?)` | none | Opened when a missed-call notification is tapped | | `setDefaultRingTimeoutMs(ms)`, `setShowMissedCallNotification(bool)`, `setRequestCallPermissions(bool)` | 45 000, `true`, `true` | Ring behaviour; see [Push Ringing](/calls/android/voip-calling) | -| `setTokenRegistrar(CHAT | CALLS | AUTO)`, `setAuthToken`, `setAutoRegisterToken` | `AUTO`, none, `true` | Which login's auth token registers the device; leave the defaults | Deep links from chat notifications: @@ -206,12 +203,10 @@ class AppFCMService : FirebaseMessagingService() { ## 6. Tokens, login and logout -You do not register the device token. The SDK fetches it from Firebase and registers it when the user logs in (under the Chat auth token when there is one, the Calls auth token otherwise), re-registers it when Firebase rotates it, and unregisters it on logout. +You do not register the device token. The SDK fetches it from Firebase and registers it when the user logs in, re-registers it when Firebase rotates it, and unregisters it on logout. Log out through the UI Kit (`CometChatUIKit.logout`) or, with the SDKs directly, log out of both the Chat SDK and the Calls SDK. That is what unregisters the token and stops a call still ringing for the old user; `CometChat.logout` alone does neither. -Call `registerToken()` / `unregisterToken()` yourself only if you set `setAutoRegisterToken(false)`. - ### Permissions and ring readiness Ask for `POST_NOTIFICATIONS` (Android 13+) after sign-in, then check what else could silence a notification or a ring: @@ -226,8 +221,6 @@ CometChatPushNotifications.ensureRingReady(this) { result -> } ``` -`verifyPushSetup` and `getDiagnostics(context)` give a redacted snapshot for bug reports (whether a provider id exists, never what it is; never a token). - ## 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. @@ -315,14 +308,14 @@ CometChat.sendCustomMessage(customMessage, object : CometChat.CallbackListener>P: Push (initiated) P->>P: Rings: notification, full-screen screen, ringtone P-->>A: Callee's device reached → "ringing…" - P->>U: Answer → CometChatOngoingCallActivity + P->>U: Answer → your answer activity → CometChatOngoingCallActivity U->>U: joinSession U-->>A: Callee joined → caller joins Note over A,U: In call. Hang-up leaves the session on both sides. @@ -50,7 +50,7 @@ sequenceDiagram Three steps, all on the [Calling Integration](/ui-kit/android/calling-integration) page: 1. `setEnableCalling(true)` in `UIKitSettings`. The UI Kit initialises the Calls SDK and logs the user into it. -2. `CometChatPushNotifications.init()` with `setProviderId` and `setAnswerActivity(CometChatOngoingCallActivity::class.java)`, plus a three-line `FirebaseMessagingService`. +2. `CometChatPushNotifications.init()` with `setProviderId` and `setAnswerActivity(CallAnswerActivity::class.java)`, a small answer activity that opens `CometChatOngoingCallActivity`, plus a three-line `FirebaseMessagingService`. 3. `POST_NOTIFICATIONS`, `RECORD_AUDIO` and `CAMERA` in the manifest, and `ensureRingReady` after sign-in. Once calling is active, the call buttons render in the [MessageHeader](/ui-kit/android/message-header): diff --git a/ui-kit/android/calling-integration.mdx b/ui-kit/android/calling-integration.mdx index ef70d6abf..550e00ade 100644 --- a/ui-kit/android/calling-integration.mdx +++ b/ui-kit/android/calling-integration.mdx @@ -5,7 +5,7 @@ description: "Add voice and video calling to your Android UI Kit application usi ## Overview -With calling enabled, the UI Kit places calls from the message header and call logs, shows the outgoing call screen, and hosts the in-call screen. Incoming calls ring through the CometChat Push Notifications SDK: it shows the call notification and the full-screen incoming-call screen, plays the ringtone, and opens the UI Kit's call screen when the user answers, with the app in the foreground, in the background or not running at all. +With calling enabled, the UI Kit places calls from the message header and call logs, shows the outgoing call screen, and hosts the in-call screen. Incoming calls ring through the CometChat Push Notifications SDK: it shows the call notification and the full-screen incoming-call screen, plays the ringtone, and opens your answer activity when the user answers, which hands the call to the UI Kit's call screen, with the app in the foreground, in the background or not running at all. Make sure you've completed the [Getting Started](/ui-kit/android/getting-started) guide before proceeding. @@ -23,13 +23,13 @@ sequenceDiagram BE-->>P: Push (initiated) P->>P: Rings: notification, full-screen screen, ringtone P-->>A: Callee's device reached → "ringing…" - P->>U: Answer → mic/camera prompt → CometChatOngoingCallActivity + P->>U: Answer → mic/camera prompt → your answer activity → CometChatOngoingCallActivity U->>U: joinSession U-->>A: Callee joined → caller joins Note over A,U: In call. Hang-up leaves the session on both sides. ``` -What you write: the UI Kit settings with calling enabled, the Push SDK configuration, a three-line Firebase service, and the manifest permissions. There is no call listener to register and no incoming-call UI to show. +What you write: the UI Kit settings with calling enabled, the Push SDK configuration, a small answer activity, a three-line Firebase service, and the manifest permissions. There is no call listener to register and no incoming-call UI to show. ## Step 1: Dependencies @@ -110,39 +110,99 @@ Run this in `Application.onCreate` from stored credentials, not only after a scr ## Step 3: Configure push ringing -Initialise the Push Notifications SDK once, after the UI Kit (the FCM provider id and the answer activity are all it needs). `CometChatOngoingCallActivity` is the UI Kit's in-call screen; registered as the answer activity it reads the answered call from its intent and joins. +Initialise the Push Notifications SDK once, after the UI Kit. It needs the FCM provider id and an answer activity: the screen the Push SDK opens when the user taps **Answer**. - - ```kotlin -import com.cometchat.uikit.kotlin.presentation.ongoingcall.ui.CometChatOngoingCallActivity - private fun initPushNotifications() { val config = PNConfiguration.Builder(APP_ID, REGION) - .setProviderId(PROVIDER_ID) // FCM provider id from the dashboard - .setAnswerActivity(CometChatOngoingCallActivity::class.java) // the UI Kit's call screen + .setProviderId(PROVIDER_ID) // FCM provider id from the dashboard + .setAnswerActivity(CallAnswerActivity::class.java) // opened on Answer, call in its intent .setNotificationSmallIcon(R.drawable.ic_notification) .build() CometChatPushNotifications.init(this, config) } ``` + +The UI Kit does not depend on the Push SDK, so the answer activity is a small class in your app. It reads the answered call from its intent, opens the UI Kit's call screen with it, and finishes: + + + +```kotlin +import com.cometchat.chat.constants.CometChatConstants +import com.cometchat.pushnotification.calls.CallBundles +import com.cometchat.uikit.core.utils.CallRingingUtils +import com.cometchat.uikit.kotlin.presentation.ongoingcall.ui.CometChatOngoingCallActivity + +class CallAnswerActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setShowWhenLocked(true) + setTurnScreenOn(true) + + val incoming = CallBundles.getCall(intent) ?: return finish() + val call = CallRingingUtils.buildIncomingCall( + sessionId = incoming.sessionId, + callType = if (incoming.isVideo) CometChatConstants.CALL_TYPE_VIDEO else CometChatConstants.CALL_TYPE_AUDIO, + receiverType = incoming.receiverType, + receiverUid = incoming.receiverUid, + callerUid = incoming.callerUid, + callerName = incoming.callerName, + callerAvatarUrl = incoming.callerAvatarUrl, + initiatedAtMs = incoming.initiatedAtMs, + conversationId = incoming.conversationId + ) + CometChatOngoingCallActivity.launchOngoingCallActivity(this, call) + finish() + } +} +``` ```kotlin +import com.cometchat.chat.constants.CometChatConstants +import com.cometchat.pushnotification.calls.CallBundles +import com.cometchat.uikit.core.utils.CallRingingUtils import com.cometchat.uikit.compose.presentation.ongoingcall.ui.CometChatOngoingCallActivity -private fun initPushNotifications() { - val config = PNConfiguration.Builder(APP_ID, REGION) - .setProviderId(PROVIDER_ID) // FCM provider id from the dashboard - .setAnswerActivity(CometChatOngoingCallActivity::class.java) // the UI Kit's call screen - .setNotificationSmallIcon(R.drawable.ic_notification) - .build() - CometChatPushNotifications.init(this, config) +class CallAnswerActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setShowWhenLocked(true) + setTurnScreenOn(true) + + val incoming = CallBundles.getCall(intent) ?: return finish() + val call = CallRingingUtils.buildIncomingCall( + sessionId = incoming.sessionId, + callType = if (incoming.isVideo) CometChatConstants.CALL_TYPE_VIDEO else CometChatConstants.CALL_TYPE_AUDIO, + receiverType = incoming.receiverType, + receiverUid = incoming.receiverUid, + callerUid = incoming.callerUid, + callerName = incoming.callerName, + callerAvatarUrl = incoming.callerAvatarUrl, + initiatedAtMs = incoming.initiatedAtMs, + conversationId = incoming.conversationId + ) + CometChatOngoingCallActivity.launchOngoingCallActivity(this, call) + finish() + } } ``` +```xml AndroidManifest.xml + +``` + Then hand every push to the SDK from your Firebase service: ```kotlin @@ -166,7 +226,7 @@ class AppFCMService : FirebaseMessagingService() { The push token is registered on login and unregistered on logout by the SDK. Do not skip call pushes when the app is in the foreground: the push is the only incoming-call signal, in every app state. Call pushes must be sent at high FCM priority. -Every option of `PNConfiguration` (ring timeout, missed-call notifications, your own ring screen, MANUAL mode) is on the Calls SDK's [Push Ringing](/calls/android/voip-calling) page. +The other `PNConfiguration` options (ring timeout, missed-call notifications, your own ring screen) are on the Calls SDK's [Push Ringing](/calls/android/voip-calling) page. ## Step 4: Permissions @@ -176,7 +236,7 @@ Every option of `PNConfiguration` (ring timeout, missed-call notifications, your ``` -The remaining permissions and components (full-screen intent, the ringing foreground service, the ring screen) come from the Push SDK's manifest. +The remaining permissions come from the Push SDK's manifest. At runtime: @@ -237,7 +297,7 @@ CometChatMessageHeader( | Waiting | "calling…", then "ringing…" when the callee's device is reached; End cancels the call; 45 s with no answer ends it as unanswered | [Outgoing Call](/ui-kit/android/outgoing-call) | | Callee declines / is busy / does not answer | The outgoing screen closes with the reason | Outgoing Call | | Incoming call | The Push SDK rings and draws the incoming-call screen | Push SDK (no UI Kit component) | -| Answer | `CometChatOngoingCallActivity` opens and joins the session | [Ongoing Call](/ui-kit/android/ongoing-call) | +| Answer | Your answer activity opens `CometChatOngoingCallActivity`, which joins the session | [Ongoing Call](/ui-kit/android/ongoing-call) | | Hang-up | The session is left; the other side's screen closes | Ongoing Call | | Call history | Call logs list from the Calls SDK; the call icon calls back | [Call Logs](/ui-kit/android/call-logs) | | Group call | The group call button sends a meeting message; tapping it joins the conference | Call Buttons, Ongoing Call | diff --git a/ui-kit/android/incoming-call.mdx b/ui-kit/android/incoming-call.mdx index 5a1fa7800..0336aa367 100644 --- a/ui-kit/android/incoming-call.mdx +++ b/ui-kit/android/incoming-call.mdx @@ -7,7 +7,7 @@ description: "Deprecated incoming call screen, kept for apps that draw the ring **Deprecated.** Incoming calls now ring through the CometChat Push Notifications SDK, which draws its own incoming-call screen in every app state, including when the app is not running. `CometChatIncomingCall` is kept only for apps that opt into drawing the ring screen themselves with `PNConfiguration.setIncomingCallActivity`. New integrations need no incoming-call component: see [Calling Integration](/ui-kit/android/calling-integration). -`CometChatIncomingCall` shows the caller's avatar, name and call type with accept and decline controls. In the current ringing flow the Push SDK owns the ringtone, the ring timeout, the pre-warmed session and the decline; this component only draws the screen and forwards Accept and Decline to the Push SDK. +`CometChatIncomingCall` shows the caller's avatar, name and call type with accept and decline controls. It is a view only: it does not ring, and the UI Kit has no dependency on the Push SDK. If you use it as your ring screen, your activity connects its Accept and Decline to the Push SDK. @@ -20,55 +20,27 @@ description: "Deprecated incoming call screen, kept for apps that draw the ring | You want | Do this | | --- | --- | | The default incoming-call screen | Nothing. The Push SDK's screen is themed by the `cometchat_pn_call_*` colour resources; the UI Kit already matches them to its palette. See [Styling the incoming-call screen](/ui-kit/android/calling-integration#styling-the-incoming-call-screen) | -| The UI Kit's incoming-call screen instead of the Push SDK's | Register the UI Kit's `CometChatCallActivity` as the ring screen (below). You get this component's view slots and style | +| This component as the ring screen | Host it in your own activity registered with `PNConfiguration.setIncomingCallActivity` (below) | | A fully custom ring screen | Your own activity with the Push SDK's `CometChatCallNotifications` API; see [Your own incoming-call screen](/calls/android/voip-calling#your-own-incoming-call-screen) | --- -## Quick Start: the UI Kit screen as the ring screen +## Quick Start: the component as the ring screen -Register `CometChatCallActivity` with the Push SDK. It reads the ringing call from the intent, shows over the lock screen, hosts `CometChatIncomingCall`, asks for the microphone and camera on Accept, answers and declines through the Push SDK, and closes itself when the call ends elsewhere. - - - +Register your ring activity alongside your [answer activity](/ui-kit/android/calling-integration#step-3-configure-push-ringing): ```kotlin lines -import com.cometchat.uikit.kotlin.calls.CometChatCallActivity -import com.cometchat.uikit.kotlin.presentation.ongoingcall.ui.CometChatOngoingCallActivity - PNConfiguration.Builder(APP_ID, REGION) .setProviderId(PROVIDER_ID) - .setAnswerActivity(CometChatOngoingCallActivity::class.java) - .setIncomingCallActivity(CometChatCallActivity::class.java) + .setAnswerActivity(CallAnswerActivity::class.java) + .setIncomingCallActivity(RingActivity::class.java) .build() ``` - - - -```kotlin lines -import com.cometchat.uikit.compose.calls.CometChatCallActivity -import com.cometchat.uikit.compose.presentation.ongoingcall.ui.CometChatOngoingCallActivity - -PNConfiguration.Builder(APP_ID, REGION) - .setProviderId(PROVIDER_ID) - .setAnswerActivity(CometChatOngoingCallActivity::class.java) - .setIncomingCallActivity(CometChatCallActivity::class.java) - .build() -``` - - - - -### Embedding the component yourself - -If you host the component in your own ring activity, that activity must also tell the Push SDK when it opens and closes, so the SDK can dismiss it when the caller cancels or the ring times out. `CallRingingUtils` wraps the Push SDK calls and converts the push's call into the `Call` the component takes. - - - +The activity reads the ringing call from its intent, tells the Push SDK when it opens and closes (so the SDK can close it when the caller cancels or the ring times out), and sends Accept and Decline to the Push SDK. On Accept the Push SDK opens your answer activity. ```kotlin lines -class MyRingActivity : AppCompatActivity() { +class RingActivity : AppCompatActivity() { private var sessionId: String? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -76,48 +48,44 @@ class MyRingActivity : AppCompatActivity() { setShowWhenLocked(true); setTurnScreenOn(true) setContentView(R.layout.activity_ring) - val call = CallRingingUtils.getCall(intent) ?: return finish() - sessionId = call.sessionId - CallRingingUtils.onRingScreenOpened(call.sessionId) { finish() } - - findViewById(R.id.incoming_call).setCall(call) - } - - override fun onDestroy() { - sessionId?.let { CallRingingUtils.onRingScreenClosed(it) } - super.onDestroy() - } -} -``` - - - - -```kotlin lines -class MyRingActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setShowWhenLocked(true); setTurnScreenOn(true) - val call = CallRingingUtils.getCall(intent) ?: return finish() - CallRingingUtils.onRingScreenOpened(call.sessionId) { finish() } - setContent { - CometChatTheme { - CometChatIncomingCall(call = call, onRejectClick = { finish() }) + val incoming = CallBundles.getCall(intent) ?: return finish() + sessionId = incoming.sessionId + CometChatCallNotifications.onIncomingCallScreenOpened(incoming.sessionId) { finish() } + + val call = CallRingingUtils.buildIncomingCall( + sessionId = incoming.sessionId, + callType = if (incoming.isVideo) CometChatConstants.CALL_TYPE_VIDEO else CometChatConstants.CALL_TYPE_AUDIO, + receiverType = incoming.receiverType, + receiverUid = incoming.receiverUid, + callerUid = incoming.callerUid, + callerName = incoming.callerName, + callerAvatarUrl = incoming.callerAvatarUrl, + initiatedAtMs = incoming.initiatedAtMs, + conversationId = incoming.conversationId + ) + findViewById(R.id.incoming_call).apply { + setCall(call) + setOnAcceptClickListener { + // Ask for RECORD_AUDIO (+ CAMERA for video) first: answer() skips the SDK prompt. + CometChatCallNotifications.answer(this@RingActivity, incoming.sessionId) + } + setOnRejectClickListener { + CometChatCallNotifications.decline(this@RingActivity, incoming.sessionId) + finish() } } } override fun onDestroy() { - CallRingingUtils.getCall(intent)?.let { CallRingingUtils.onRingScreenClosed(it.sessionId) } + sessionId?.let { CometChatCallNotifications.onIncomingCallScreenClosed(it) } super.onDestroy() } } ``` - - +In Compose, pass the same calls as `onAcceptClick` and `onRejectClick` to `CometChatIncomingCall(call = call, ...)` inside `setContent`. -Prerequisites: `CometChatUIKit.init()` with `setEnableCalling(true)`, the Push SDK initialised with `setAnswerActivity(CometChatOngoingCallActivity::class.java)`, and a user logged in. +Prerequisites: `CometChatUIKit.init()` with `setEnableCalling(true)`, the Push SDK initialised with an answer activity, and a user logged in. --- @@ -127,7 +95,7 @@ Prerequisites: `CometChatUIKit.init()` with `setEnableCalling(true)`, the Push S #### `onAcceptClick` -Fires when the user taps Accept. By default the component answers through the Push SDK (`CometChatCallNotifications.answer`), which stops the ring and opens the answer activity (`CometChatOngoingCallActivity`), and the ring screen is closed by the Push SDK. Provide the callback to replace that behaviour. Ask for the microphone (and camera) before answering: the Push SDK only prompts for its notification's Answer action. +Fires when the user taps Accept. By default the component opens `CometChatOngoingCallActivity` itself. On a Push SDK ring screen, set it to call `CometChatCallNotifications.answer(context, sessionId)` instead, so the Push SDK stops the ring and opens your answer activity. Ask for the microphone (and camera) before answering: the Push SDK only prompts for its notification's Answer action. @@ -155,7 +123,7 @@ CometChatIncomingCall( #### `onRejectClick` -Fires when the user taps Decline. By default the component declines through the Push SDK (`CometChatCallNotifications.decline`), which sends `rejected`, or `busy` when the user is in another call, through a queue that survives the process dying. Provide the callback to replace that behaviour. +Fires when the user taps Decline. By default the component declines through the Calls SDK as `rejected`. On a Push SDK ring screen, set it to call `CometChatCallNotifications.decline(context, sessionId)` instead, so the Push SDK stops the ring and sends `rejected` or `busy`. @@ -221,7 +189,7 @@ CometChatIncomingCall( | --- | --- | | `IncomingCallListener.onIncomingCallEnded` for this session | The ring ended elsewhere (caller cancelled, answered on another device, timed out): `rejectedCall` is emitted so the screen closes | -The component does not open itself on an incoming call. The Push SDK launches the registered ring activity; the Chat SDK's `CallListener` is superseded and not used. +The component does not open itself on an incoming call. The Push SDK launches the activity registered with `setIncomingCallActivity`; the Chat SDK's `CallListener` is superseded and not used. --- diff --git a/ui-kit/android/llms-android-v6.mdx b/ui-kit/android/llms-android-v6.mdx index 3d019dca6..8cc32d855 100644 --- a/ui-kit/android/llms-android-v6.mdx +++ b/ui-kit/android/llms-android-v6.mdx @@ -100,7 +100,7 @@ attributes, long-tail components, theming tokens, or feature enablement. ## Components — calling - [Call Buttons](/ui-kit/android/call-buttons) - [Outgoing Call](/ui-kit/android/outgoing-call) -- [Ongoing Call](/ui-kit/android/ongoing-call) — the in-call screen; also the Push SDK answer activity +- [Ongoing Call](/ui-kit/android/ongoing-call) — the in-call screen; your answer activity opens it for an answered incoming call - [Call Logs](/ui-kit/android/call-logs) - [Incoming Call](/ui-kit/android/incoming-call) — deprecated; incoming calls ring through the Push Notifications SDK, no component needed @@ -136,7 +136,7 @@ the symptom, not the component. | Toolbar renders **under the status bar** / title collides with the clock | [Conversation + message view](/ui-kit/android/conversation-message-view) · [One-to-one chat](/ui-kit/android/one-to-one-chat) — `enableEdgeToEdge()` needs insets CONSUMED | | Composer crashes / `ActivityResultLauncher not initialized` | [Troubleshooting](/ui-kit/android/troubleshooting) — build the composer in `onCreate` | | Calling crashes: `Please call the CometChatCalls.init()` | [Calling integration](/ui-kit/android/calling-integration) — `setEnableCalling(true)` is missing | -| Incoming calls never ring | [Calling integration](/ui-kit/android/calling-integration) — `CometChatPushNotifications.init` with `setAnswerActivity(CometChatOngoingCallActivity::class.java)` + a `FirebaseMessagingService` that calls `handlePushNotification`; the Chat SDK `CallListener` is superseded and fires nothing | +| Incoming calls never ring | [Calling integration](/ui-kit/android/calling-integration) — `CometChatPushNotifications.init` with `setAnswerActivity(CallAnswerActivity::class.java)` (your activity that opens `CometChatOngoingCallActivity`) + a `FirebaseMessagingService` that calls `handlePushNotification`; the Chat SDK `CallListener` is superseded and fires nothing | | Components render but stay empty | init/login did not resolve — [Getting started (Kotlin)](/ui-kit/android/getting-started-kotlin) / [(Compose)](/ui-kit/android/getting-started-jetpack) | | Keyboard covers the composer | `windowSoftInputMode="adjustResize"` — [One-to-one chat](/ui-kit/android/one-to-one-chat) | | Colours ignored / theme not applied | [Theme introduction](/ui-kit/android/theme-introduction) | diff --git a/ui-kit/android/ongoing-call.mdx b/ui-kit/android/ongoing-call.mdx index 33f2d8048..d5a795eb2 100644 --- a/ui-kit/android/ongoing-call.mdx +++ b/ui-kit/android/ongoing-call.mdx @@ -1,9 +1,9 @@ --- title: "Ongoing Call" -description: "In-call screen that joins the call session and renders the Calls SDK's call UI; also the activity the Push SDK opens when the user answers." +description: "In-call screen that joins the call session and renders the Calls SDK's call UI, for outgoing, answered incoming and group calls." --- -`CometChatOngoingCall` renders the active call: the Calls SDK's video tiles and controls (mute, camera, switch camera, end). `CometChatOngoingCallActivity` hosts it full-screen, supports picture-in-picture, and is the **answer activity** for push ringing: when the user answers an incoming call, the Push SDK opens it with the call in its intent and it joins the session. +`CometChatOngoingCall` renders the active call: the Calls SDK's video tiles and controls (mute, camera, switch camera, end). `CometChatOngoingCallActivity` hosts it full-screen, supports picture-in-picture, and is where an answered incoming call ends up: your app's answer activity opens it with the answered call, and it joins the session. --- @@ -13,7 +13,7 @@ Every call ends up here: | Path | How the screen opens | |------|----------------------| -| Incoming call answered | The Push SDK starts `CometChatOngoingCallActivity` (registered with `PNConfiguration.setAnswerActivity`). It reads the answered call from the intent and joins | +| Incoming call answered | The Push SDK starts your answer activity (`PNConfiguration.setAnswerActivity`), which builds the call with `CallRingingUtils.buildIncomingCall` and calls `CometChatOngoingCallActivity.launchOngoingCallActivity(context, call)` | | Outgoing call answered | The [Outgoing Call](/ui-kit/android/outgoing-call) screen hands over once the callee joins: XML Views embed `CometChatOngoingCall` in the same activity, Compose launches `CometChatOngoingCallActivity` | | Group call / meeting | [Call Buttons](/ui-kit/android/call-buttons) send a meeting message; tapping it launches the activity with `CallWorkFlow.MEETING` | | Your own flow | `CometChatOngoingCallActivity.launchOngoingCallActivity(context, sessionId, callType)` or the component directly | @@ -51,18 +51,28 @@ CometChatOngoingCallActivity.launchOngoingCallActivity( ## Quick Start -### As the answer activity (incoming calls) +### From your answer activity (incoming calls) -Register it once in the Push SDK configuration. No other code: the activity reads the session id and call type from the intent, asks for the microphone and camera, and calls `joinSession`. +The Push SDK opens your answer activity with the answered call in its intent. Turn it into a UI Kit `Call` and launch this screen; it asks for the microphone and camera and joins: ```kotlin lines -PNConfiguration.Builder(APP_ID, REGION) - .setProviderId(PROVIDER_ID) - .setAnswerActivity(CometChatOngoingCallActivity::class.java) - .build() +val incoming = CallBundles.getCall(intent) ?: return finish() +val call = CallRingingUtils.buildIncomingCall( + sessionId = incoming.sessionId, + callType = if (incoming.isVideo) CometChatConstants.CALL_TYPE_VIDEO else CometChatConstants.CALL_TYPE_AUDIO, + receiverType = incoming.receiverType, + receiverUid = incoming.receiverUid, + callerUid = incoming.callerUid, + callerName = incoming.callerName, + callerAvatarUrl = incoming.callerAvatarUrl, + initiatedAtMs = incoming.initiatedAtMs, + conversationId = incoming.conversationId +) +CometChatOngoingCallActivity.launchOngoingCallActivity(this, call) +finish() ``` -See [Calling Integration](/ui-kit/android/calling-integration) for the full setup. +See [Calling Integration](/ui-kit/android/calling-integration#step-3-configure-push-ringing) for the full answer activity and its manifest entry. ### Launching it yourself diff --git a/ui-kit/android/outgoing-call.mdx b/ui-kit/android/outgoing-call.mdx index 2565870d8..5accc6673 100644 --- a/ui-kit/android/outgoing-call.mdx +++ b/ui-kit/android/outgoing-call.mdx @@ -98,7 +98,7 @@ When you host the component yourself, give it a lifecycle owner so it can attach | State | Subtitle | What happened | | --- | --- | --- | | Calling | "calling …" | `initiateCall` succeeded; the backend is pushing the callee | -| Ringing | "ringing …" | The callee's device received the push and pre-warmed the session (`onParticipantConnected`) | +| Ringing | "ringing …" | The callee's device received the push (`onParticipantConnected`) | | Accepted | — | The callee joined (`onParticipantJoined`). The End button is disabled and the ongoing call screen opens | | Ended | — | The call ended without an answer. `endReason` on the ViewModel says why: `rejected`, `busy`, `unanswered`, `cancelled` or `ended` | diff --git a/ui-kit/android/troubleshooting.mdx b/ui-kit/android/troubleshooting.mdx index c0e5468f6..1b341fcd1 100644 --- a/ui-kit/android/troubleshooting.mdx +++ b/ui-kit/android/troubleshooting.mdx @@ -80,10 +80,10 @@ description: "Common failure modes and fixes for the CometChat Android V6 UI Kit | --- | --- | --- | | Call buttons not appearing | `enableCalling` not set | `setEnableCalling(true)` in `UIKitSettings` (or `"enableCalling": true` in `cometchat-settings.json`). See [Calling Integration](/ui-kit/android/calling-integration) | | Call logs show a silent error after enabling calling | User logged in before calling was enabled, so the Calls SDK never logged in | `CometChatUIKit.logout()` then `CometChatUIKit.login()` once | -| Incoming calls never ring | Push ringing not configured, or the push not handed to the SDK | `CometChatPushNotifications.init` with `setProviderId` + `setAnswerActivity(CometChatOngoingCallActivity::class.java)`, and `handlePushNotification(this, data, sentTime)` for every push in your `FirebaseMessagingService`. See [Push Ringing](/calls/android/voip-calling) | +| Incoming calls never ring | Push ringing not configured, or the push not handed to the SDK | `CometChatPushNotifications.init` with `setProviderId` + `setAnswerActivity(...)`, and `handlePushNotification(this, data, sentTime)` for every push in your `FirebaseMessagingService`. See [Push Ringing](/calls/android/voip-calling) | | Rings on an emulator or a fresh install, but not on a user's phone | Device settings silence the ring | Run `CometChatPushNotifications.ensureRingReady(activity)` after sign-in and show any result other than `READY` (notifications off, muted channel, full-screen intent revoked, Do Not Disturb) | | Rings, then the process dies before Answer | Call push sent at normal FCM priority | Send call pushes at high priority | -| Answer opens the app but the call does not join | Answer activity not the UI Kit's, or SDK not initialised on a cold start | Register `CometChatOngoingCallActivity` as the answer activity and run `CometChatUIKit.init` in `Application.onCreate` from stored credentials | +| Answer opens the app but the call does not join | The answer activity does not hand the call to the UI Kit, or the SDK is not initialised on a cold start | In your answer activity, build the call with `CallRingingUtils.buildIncomingCall` and call `CometChatOngoingCallActivity.launchOngoingCallActivity(this, call)` (see [Calling Integration](/ui-kit/android/calling-integration#step-3-configure-push-ringing)), and run `CometChatUIKit.init` in `Application.onCreate` from stored credentials | | Caller's screen stays on "calling…" and never shows "ringing…" | Callee's device not reached (push not delivered) | Check the callee's `ensureRingReady`, provider id and host; the caller times out as unanswered after 45 s | | Audio call opens the caller's camera | Custom call start without a session type | Place calls with `CallInitiationUtils.initiateUserCall`, or set `setSessionType(SessionType.VOICE)` in the `InitiateCallOptions` session settings | | Ring keeps playing after logout, or declines fail after switching users | Logged out with `CometChat.logout` only | Use `CometChatUIKit.logout()` | diff --git a/ui-kit/android/upgrading-from-v5.mdx b/ui-kit/android/upgrading-from-v5.mdx index cd74d49cc..9e495cc53 100644 --- a/ui-kit/android/upgrading-from-v5.mdx +++ b/ui-kit/android/upgrading-from-v5.mdx @@ -375,7 +375,7 @@ V6 rings incoming calls through the CometChat Push Notifications SDK instead of | V5 | V6 | | --- | --- | | `CometChat.addCallListener(...)` in the Application class showing `CometChatIncomingCall` | Remove it. The Push SDK rings and draws the incoming-call screen in every app state | -| Push SDK 1.0 `setVoIPEnabled(true)` + `setOnCallAnsweredHandler` (which called `CometChat.acceptCall`) | `PNConfiguration.Builder(...).setProviderId(...).setAnswerActivity(CometChatOngoingCallActivity::class.java)` | +| Push SDK 1.0 `setVoIPEnabled(true)` + `setOnCallAnsweredHandler` (which called `CometChat.acceptCall`) | `PNConfiguration.Builder(...).setProviderId(...).setAnswerActivity(CallAnswerActivity::class.java)`, where `CallAnswerActivity` opens `CometChatOngoingCallActivity`; see [Calling Integration](/ui-kit/android/calling-integration#step-3-configure-push-ringing) | | `FirebaseMessagingService` that skipped call pushes in the foreground and registered tokens manually | `handlePushNotification(this, data, sentTime)` for every push; tokens are automatic | | `CometChatIncomingCall` as the primary incoming UI | Deprecated; kept only for `setIncomingCallActivity` | | `CometChat.logout` | `CometChatUIKit.logout` (also logs out of the Calls SDK) | From e4e881f5a66f8a2706102ec3bea8ad9bbaddc560 Mon Sep 17 00:00:00 2001 From: Hritika Date: Fri, 16 Oct 2026 18:26:03 +0530 Subject: [PATCH 7/7] docs(android): theme the incoming-call screen in code (CometChatPNIncomingCallTheme) The Push SDK's incoming-call screen can now be themed in code, light and dark, and the UI Kit can pass its own theme to it. The docs still said there was no runtime theme API and listed six colour resources. - Push Ringing: setIncomingCallTheme in the setter table; the theming section now covers CometChatPNIncomingCallTheme (Mode, Colors, Kotlin and Java), saving across a killed process, the theme -> resource -> default order, and all eleven colour resources with their setters. Notes that the class is not the @style/CometChatPNIncomingCallTheme window style. - Push Notifications: setIncomingCallTheme row. - UI Kit calling integration: CometChatTheme.getIncomingCallTheme() and Compose incomingCallTheme(), the app's mapping function, the one-line setIncomingCallTheme, and which UI Kit colour feeds each field. - Color resources: eleven colours with the UI Kit colour, light and dark. - Incoming Call (deprecated): CometChatIncomingCallStyle -> theme field map, including what has no equivalent. - Call features: incoming call paragraph. Co-Authored-By: Claude Opus 5.5 --- calls/android/voip-calling.mdx | 116 +++++++++++++++++-- notifications/android-push-notifications.mdx | 1 + ui-kit/android/call-features.mdx | 2 +- ui-kit/android/calling-integration.mdx | 78 +++++++++++-- ui-kit/android/color-resources.mdx | 32 +++-- ui-kit/android/incoming-call.mdx | 16 ++- 6 files changed, 209 insertions(+), 36 deletions(-) diff --git a/calls/android/voip-calling.mdx b/calls/android/voip-calling.mdx index 8d5f5695d..a4512282c 100644 --- a/calls/android/voip-calling.mdx +++ b/calls/android/voip-calling.mdx @@ -82,6 +82,7 @@ CometChatPushNotifications.init( | `setProviderId(id)` | none | The dashboard FCM provider the token is registered under | | `setAnswerActivity(Class)` | none, **required** | Started when the user answers, with the call in its extras | | `setIncomingCallActivity(Class?)` | the SDK's own screen | Your full-screen ring screen; see [Your own incoming-call screen](#your-own-incoming-call-screen) | +| `setIncomingCallTheme(theme?)` | none | Colours of the SDK's incoming-call screen and call notifications, light and dark; see [Theming the SDK's incoming-call screen](#theming-the-sdks-incoming-call-screen) | | `setMissedCallActivity(Class?)` | none | Opened when a missed-call notification is tapped. Without it an individual missed-call notification has no tap action | | `setNotificationSmallIcon(res)` | none | Status-bar icon for every notification | | `setDefaultRingTimeoutMs(ms)` | 45 000 | Ring window when the push carries no `ringTimeout`; 1 s to 10 min | @@ -393,16 +394,107 @@ Without `onIncomingCallScreenOpened`, a cancelled call leaves your screen on the ## Theming the SDK's incoming-call screen -The SDK's screen and notifications read their look from resources. Override any of them by name in your app's `res/` (an app's resource wins over the library's); there is no runtime theme API. +The SDK's incoming-call screen and its call notifications take each colour from, in order: -| Colour | Default | Used for | -|--------|---------|----------| -| `cometchat_pn_call_background` | `#0E0E10` | Screen background | -| `cometchat_pn_call_text` | `#F2F2F2` | Caller name | -| `cometchat_pn_call_text_secondary` | `#9A9AA1` | "Incoming audio call" line | -| `cometchat_pn_call_avatar` | `#3B5BDB` | Initials circle | -| `cometchat_pn_call_accept` | `#1B8F3A` | Answer button, notification Answer action | -| `cometchat_pn_call_decline` | `#C62828` | Decline button, notification Decline action | +1. a `CometChatPNIncomingCallTheme` set in code; +2. the `cometchat_pn_call_*` colour resources, which your app (or a UI Kit) can override by name; +3. the SDK's defaults. + +Each colour is looked up on its own, so a theme can set only the colours it cares about. + +### In code + +Use this when your colours live in code, differ between light and dark, or change while the app runs. + + + +```kotlin +val theme = CometChatPNIncomingCallTheme.Builder() + .setMode(CometChatPNIncomingCallTheme.Mode.SYSTEM) // SYSTEM (default), LIGHT or DARK + .setLightColors( + CometChatPNIncomingCallTheme.Colors.Builder() + .setBackground(0xFFFFFFFF.toInt()) + .setCallerName(0xFF141414.toInt()) + .setAcceptButton(0xFF09C26F.toInt()) + .setDeclineButton(0xFFF44649.toInt()) + .build() + ) + .setDarkColors( + CometChatPNIncomingCallTheme.Colors.Builder() + .setBackground(0xFF141414.toInt()) + .setCallerName(0xFFFFFFFF.toInt()) + .build() + ) + .build() + +CometChatPushNotifications.init( + this, + PNConfiguration.Builder(APP_ID, REGION) + // ... + .setIncomingCallTheme(theme) + .build() +) + +// Later, when your app's theme changes. Pass null to remove it. +CometChatPushNotifications.setIncomingCallTheme(context, newTheme) +``` + + +```java +CometChatPNIncomingCallTheme theme = new CometChatPNIncomingCallTheme.Builder() + .setMode(CometChatPNIncomingCallTheme.Mode.SYSTEM) + .setLightColors(new CometChatPNIncomingCallTheme.Colors.Builder() + .setBackground(0xFFFFFFFF) + .setCallerName(0xFF141414) + .setAcceptButton(0xFF09C26F) + .setDeclineButton(0xFFF44649) + .build()) + .setDarkColors(new CometChatPNIncomingCallTheme.Colors.Builder() + .setBackground(0xFF141414) + .setCallerName(0xFFFFFFFF) + .build()) + .build(); + +CometChatPushNotifications.init( + this, + new PNConfiguration.Builder(APP_ID, REGION) + // ... + .setIncomingCallTheme(theme) + .build() +); + +// Later, when your app's theme changes. Pass null to remove it. +CometChatPushNotifications.setIncomingCallTheme(context, newTheme); +``` + + + +- **Mode.** `SYSTEM` follows the device's dark theme. `LIGHT` and `DARK` fix the colours, and the resource fallback then resolves in that mode too. +- **Saved.** The theme is saved when you pass it, so it also applies when a push starts your app from killed. +- **Kept across `init`.** An `init` without a theme keeps the saved one. Remove it with `setIncomingCallTheme(context, null)`. +- **Only what you set.** The screen applies only the colours the theme sets. If you replaced the layout, your own styling stays for everything else. + + +`CometChatPNIncomingCallTheme` the class is not the style resource `@style/CometChatPNIncomingCallTheme`. The style resource is the screen's window theme, and can still be overridden by name. + + +### As resources + +Override any of these by name in your app's `res/values/colors.xml` (and `res/values-night/colors.xml` for dark). An app's resource wins over the library's. Each colour also has a matching setter on `CometChatPNIncomingCallTheme.Colors.Builder`. + +| Colour resource | `Colors.Builder` setter | Default | Used for | +|--------|---------|---------|----------| +| `cometchat_pn_call_background` | `setBackground` | `#0E0E10` | Screen, status bar and navigation bar | +| `cometchat_pn_call_text` | `setCallerName` | `#F2F2F2` | Caller name | +| `cometchat_pn_call_text_secondary` | `setSubtitle` | `#9A9AA1` | "Incoming audio call" line | +| `cometchat_pn_call_avatar` | `setAvatarBackground` | `#3B5BDB` | Initials circle, on the screen and in the notification | +| `cometchat_pn_call_avatar_text` | `setAvatarText` | `#FFFFFF` | Initials | +| `cometchat_pn_call_accept` | `setAcceptButton` | `#1B8F3A` | Answer button, ring notification accent | +| `cometchat_pn_call_accept_icon` | `setAcceptIcon` | `#FFFFFF` | Icon on the Answer button | +| `cometchat_pn_call_accept_label` | `setAcceptLabel` | `text_secondary` | "Answer" label under the button | +| `cometchat_pn_call_decline` | `setDeclineButton` | `#C62828` | Decline button, missed-call accent | +| `cometchat_pn_call_decline_icon` | `setDeclineIcon` | `#FFFFFF` | Icon on the Decline button | +| `cometchat_pn_call_decline_label` | `setDeclineLabel` | `text_secondary` | "Decline" label under the button | ```xml res/values/colors.xml #FFFFFF @@ -411,10 +503,12 @@ The SDK's screen and notifications read their look from resources. Override any #F44649 ``` -The strings, drawables and layout of the screen (all prefixed `cometchat_pn_`) can be overridden the same way. +The strings, drawables and layout of the screen (all prefixed `cometchat_pn_`) can be overridden the same way. A replacement layout must keep the ids `cometchat_pn_initials`, `cometchat_pn_caller_name`, `cometchat_pn_call_type`, `cometchat_pn_answer` and `cometchat_pn_decline`. + +On Android 12 and later the Answer and Decline buttons in the notification are drawn by the system, so there only the accent colour and the avatar take your colours. -If you use the CometChat UI Kit, it already overrides the six colours with its own light and dark palette. Your app's override still wins over the UI Kit's. +If you use the CometChat UI Kit, it already overrides the colour resources with its own light and dark palette, and can pass your UI Kit theme as a `CometChatPNIncomingCallTheme`; see [Calling integration](/ui-kit/android/calling-integration). Your app's own theme and resources still win over the UI Kit's. ## Knowing about a call in your own code diff --git a/notifications/android-push-notifications.mdx b/notifications/android-push-notifications.mdx index 47d8412ac..5cb2bf709 100644 --- a/notifications/android-push-notifications.mdx +++ b/notifications/android-push-notifications.mdx @@ -151,6 +151,7 @@ CometChatPushNotifications.init(this, config) | `setInlineReplyEnabled(bool)` | `true` | Reply action on chat notifications | | `setAnswerActivity(Class)` | none; required with the calls module | Opened when the user answers a call. UI Kit apps use a small activity that opens `CometChatOngoingCallActivity`; see [Calling Integration](/ui-kit/android/calling-integration#step-3-configure-push-ringing) | | `setIncomingCallActivity(Class?)` | the SDK's screen | Your own incoming-call screen | +| `setIncomingCallTheme(theme?)` | none | Colours of the SDK's incoming-call screen and call notifications, light and dark. Change it later with `CometChatPushNotifications.setIncomingCallTheme(context, theme)`; see [Theming the SDK's incoming-call screen](/calls/android/voip-calling#theming-the-sdks-incoming-call-screen) | | `setMissedCallActivity(Class?)` | none | Opened when a missed-call notification is tapped | | `setDefaultRingTimeoutMs(ms)`, `setShowMissedCallNotification(bool)`, `setRequestCallPermissions(bool)` | 45 000, `true`, `true` | Ring behaviour; see [Push Ringing](/calls/android/voip-calling) | diff --git a/ui-kit/android/call-features.mdx b/ui-kit/android/call-features.mdx index 8dd437b62..683d1f5e8 100644 --- a/ui-kit/android/call-features.mdx +++ b/ui-kit/android/call-features.mdx @@ -79,7 +79,7 @@ The `CometChatCallButtons` component provides users with quick access to initiat ### Incoming Call -Incoming calls are rung and drawn by the Push Notifications SDK, so there is no UI Kit component to place. Its screen is themed by the `cometchat_pn_call_*` colour resources, which the UI Kit already sets to its palette. The deprecated `CometChatIncomingCall` component remains for apps that register their own ring screen. +Incoming calls are rung and drawn by the Push Notifications SDK, so there is no UI Kit component to place. Its screen uses the UI Kit's default colours, and follows your own UI Kit theme, light and dark, once you pass it with `setIncomingCallTheme` ([Styling the incoming-call screen](/ui-kit/android/calling-integration#styling-the-incoming-call-screen)). The deprecated `CometChatIncomingCall` component remains for apps that register their own ring screen. diff --git a/ui-kit/android/calling-integration.mdx b/ui-kit/android/calling-integration.mdx index 550e00ade..c84e96ed9 100644 --- a/ui-kit/android/calling-integration.mdx +++ b/ui-kit/android/calling-integration.mdx @@ -226,7 +226,7 @@ class AppFCMService : FirebaseMessagingService() { The push token is registered on login and unregistered on logout by the SDK. Do not skip call pushes when the app is in the foreground: the push is the only incoming-call signal, in every app state. Call pushes must be sent at high FCM priority. -The other `PNConfiguration` options (ring timeout, missed-call notifications, your own ring screen) are on the Calls SDK's [Push Ringing](/calls/android/voip-calling) page. +To colour the incoming-call screen with your UI Kit theme, see [Styling the incoming-call screen](#styling-the-incoming-call-screen). The other `PNConfiguration` options (ring timeout, missed-call notifications, your own ring screen) are on the Calls SDK's [Push Ringing](/calls/android/voip-calling) page. ## Step 4: Permissions @@ -318,18 +318,76 @@ lifecycleScope.launch { ## Styling the incoming-call screen -The incoming-call screen belongs to the Push SDK and is themed by resources, not by the UI Kit theme. The UI Kit ships overrides for the six colour resources so the screen matches its light and dark palette; your app's own override wins: +The incoming-call screen belongs to the Push SDK, which draws it even when your app is not running, so it cannot read the UI Kit theme at that moment. Instead you pass it your UI Kit theme once, as a `CometChatPNIncomingCallTheme`, when you initialise push. Light and dark are both included, and the screen and the call notifications then follow your theme. -```xml res/values/colors.xml -#FFFFFF -#141414 -#727272 -#6852D6 -#09C26F -#F44649 +The UI Kit reads its own theme into a `CometChatIncomingCallTheme`: + + + +```kotlin +// Reads your app's manifest theme (based on CometChatTheme.DayNight), light and dark +val uikitTheme = CometChatTheme.getIncomingCallTheme(this) ``` + + +```kotlin +// The same colour schemes you give CometChatTheme { } +val uikitTheme = incomingCallTheme(light = lightScheme, dark = darkScheme) +``` + + + +The UI Kit has no dependency on the Push SDK, so a small function in your app copies the colours across. The field names match one to one: + +```kotlin calls/IncomingCallTheme.kt +fun CometChatIncomingCallTheme.toCometChatPNIncomingCallTheme(): CometChatPNIncomingCallTheme = + CometChatPNIncomingCallTheme.Builder() + .setLightColors(light.toPNColors()) + .setDarkColors(dark.toPNColors()) + .build() + +private fun CometChatIncomingCallTheme.Colors.toPNColors(): CometChatPNIncomingCallTheme.Colors = + CometChatPNIncomingCallTheme.Colors.Builder() + .setBackground(background) + .setCallerName(callerName) + .setSubtitle(subtitle) + .setAvatarBackground(avatarBackground) + .setAvatarText(avatarText) + .setAcceptButton(acceptButton) + .setAcceptIcon(acceptIcon) + .setAcceptLabel(acceptLabel) + .setDeclineButton(declineButton) + .setDeclineIcon(declineIcon) + .setDeclineLabel(declineLabel) + .build() +``` + +Then add one line to the configuration from [Step 3](#step-3-configure-push-ringing): + +```kotlin +val config = PNConfiguration.Builder(APP_ID, REGION) + // ... + .setIncomingCallTheme(uikitTheme.toCometChatPNIncomingCallTheme()) + .build() +``` + +If your app changes its UI Kit colours while running, pass the new theme with `CometChatPushNotifications.setIncomingCallTheme(context, theme)`. + +Each field comes from one UI Kit colour: + +| Incoming-call field | UI Kit colour | +|---|---| +| `background` | `backgroundColor1` | +| `callerName` | `textColorPrimary` | +| `subtitle`, `acceptLabel`, `declineLabel` | `textColorSecondary` | +| `avatarBackground` | `primaryColor` | +| `avatarText`, `acceptIcon`, `declineIcon` | `colorWhite` | +| `acceptButton` | `successColor` | +| `declineButton` | `errorColor` | + +Without this line, the screen still uses the UI Kit's default colours. The UI Kit overrides the `cometchat_pn_call_*` colour resources with its light and dark palette at build time. Those overrides win over the Push SDK's defaults only when your app lists the UI Kit dependency before the Push SDK artifacts. Your app's own overrides always win. -Strings, drawables, the theme and the layout can be overridden the same way; the full list is under [Theming the SDK's incoming-call screen](/calls/android/voip-calling#theming-the-sdks-incoming-call-screen). To draw the screen yourself, register the UI Kit's `CometChatCallActivity` or your own activity with `setIncomingCallActivity`; see the deprecated [Incoming Call](/ui-kit/android/incoming-call) component. +To change a single colour, override its resource, or chain any `CometChatPNIncomingCallTheme.Colors.Builder` setter after mapping. The full list of colours, strings, drawables and the layout is under [Theming the SDK's incoming-call screen](/calls/android/voip-calling#theming-the-sdks-incoming-call-screen). To draw the screen yourself, register the UI Kit's `CometChatCallActivity` or your own activity with `setIncomingCallActivity`; see the deprecated [Incoming Call](/ui-kit/android/incoming-call) component. ## Upgrading from an app that used `CometChat.addCallListener` diff --git a/ui-kit/android/color-resources.mdx b/ui-kit/android/color-resources.mdx index aa542a7a2..86f6987e1 100644 --- a/ui-kit/android/color-resources.mdx +++ b/ui-kit/android/color-resources.mdx @@ -222,15 +222,23 @@ This means overriding a neutral color automatically updates all tokens that refe ## Incoming Call Screen Colors -The incoming-call screen is drawn by the Push Notifications SDK, not by the UI Kit theme. It reads six colour resources, which the UI Kit overrides with its palette in light and dark mode. Override them again in your app's `res/values/` and `res/values-night/` to change the screen; your app's values win. - -| Resource | UI Kit light | UI Kit dark | Used for | -| --- | --- | --- | --- | -| `cometchat_pn_call_background` | #FFFFFF | #141414 | Screen background | -| `cometchat_pn_call_text` | #141414 | #FFFFFF | Caller name | -| `cometchat_pn_call_text_secondary` | #727272 | #989898 | Call type line | -| `cometchat_pn_call_avatar` | #6852D6 | #6852D6 | Initials circle | -| `cometchat_pn_call_accept` | #09C26F | #0B9F5D | Answer button and notification action | -| `cometchat_pn_call_decline` | #F44649 | #C73C3E | Decline button and notification action | - -These are resources, so a theme set at runtime on the UI Kit does not reach the ring screen. Strings, drawables and the layout can be overridden the same way; see [Theming the SDK's incoming-call screen](/calls/android/voip-calling#theming-the-sdks-incoming-call-screen). +The incoming-call screen is drawn by the Push Notifications SDK, which cannot read the UI Kit theme when a call arrives with the app killed. It takes its colours from two places: + +- **A theme passed in code.** `CometChatTheme.getIncomingCallTheme(context)` (XML Views) or `incomingCallTheme(light, dark)` (Compose) reads your UI Kit theme, light and dark, and your app passes it to the Push SDK with `setIncomingCallTheme`. A palette changed in code, including at runtime, reaches the screen this way. See [Styling the incoming-call screen](/ui-kit/android/calling-integration#styling-the-incoming-call-screen). +- **Colour resources.** These apply to any colour the theme doesn't set. The UI Kit overrides them with its palette, and your app's `res/values/` and `res/values-night/` overrides win over both. + +| Resource | UI Kit colour | UI Kit light | UI Kit dark | Used for | +| --- | --- | --- | --- | --- | +| `cometchat_pn_call_background` | `backgroundColor1` | #FFFFFF | #141414 | Screen background | +| `cometchat_pn_call_text` | `textColorPrimary` | #141414 | #FFFFFF | Caller name | +| `cometchat_pn_call_text_secondary` | `textColorSecondary` | #727272 | #989898 | Call type line | +| `cometchat_pn_call_avatar` | `primaryColor` | #6852D6 | #6852D6 | Initials circle | +| `cometchat_pn_call_avatar_text` | `colorWhite` | #FFFFFF | #FFFFFF | Initials | +| `cometchat_pn_call_accept` | `successColor` | #09C26F | #0B9F5D | Answer button and notification accent | +| `cometchat_pn_call_accept_icon` | `colorWhite` | #FFFFFF | #FFFFFF | Icon on the Answer button | +| `cometchat_pn_call_accept_label` | `textColorSecondary` | #727272 | #989898 | "Answer" label | +| `cometchat_pn_call_decline` | `errorColor` | #F44649 | #C73C3E | Decline button and missed-call accent | +| `cometchat_pn_call_decline_icon` | `colorWhite` | #FFFFFF | #FFFFFF | Icon on the Decline button | +| `cometchat_pn_call_decline_label` | `textColorSecondary` | #727272 | #989898 | "Decline" label | + +Strings, drawables and the layout can be overridden the same way; see [Theming the SDK's incoming-call screen](/calls/android/voip-calling#theming-the-sdks-incoming-call-screen). diff --git a/ui-kit/android/incoming-call.mdx b/ui-kit/android/incoming-call.mdx index 0336aa367..c638a4104 100644 --- a/ui-kit/android/incoming-call.mdx +++ b/ui-kit/android/incoming-call.mdx @@ -19,7 +19,7 @@ description: "Deprecated incoming call screen, kept for apps that draw the ring | You want | Do this | | --- | --- | -| The default incoming-call screen | Nothing. The Push SDK's screen is themed by the `cometchat_pn_call_*` colour resources; the UI Kit already matches them to its palette. See [Styling the incoming-call screen](/ui-kit/android/calling-integration#styling-the-incoming-call-screen) | +| The default incoming-call screen | Nothing. The Push SDK draws it, in the UI Kit's default colours. To follow your own UI Kit theme, light and dark, pass it with `setIncomingCallTheme`; see [Styling the incoming-call screen](/ui-kit/android/calling-integration#styling-the-incoming-call-screen) | | This component as the ring screen | Host it in your own activity registered with `PNConfiguration.setIncomingCallActivity` (below) | | A fully custom ring screen | Your own activity with the Push SDK's `CometChatCallNotifications` API; see [Your own incoming-call screen](/calls/android/voip-calling#your-own-incoming-call-screen) | @@ -404,7 +404,19 @@ CometChatIncomingCall( -`CometChatIncomingCallStyle` is deprecated with the component; it only styles this screen. The Push SDK's own screen is themed by the `cometchat_pn_call_*` resources instead. +`CometChatIncomingCallStyle` is deprecated with the component; it only styles this screen. The Push SDK's own screen is themed with `CometChatPNIncomingCallTheme` (see [Styling the incoming-call screen](/ui-kit/android/calling-integration#styling-the-incoming-call-screen)). Its colours map like this: + +| `CometChatIncomingCallStyle` | `CometChatPNIncomingCallTheme.Colors` | +| --- | --- | +| `backgroundColor` | `background` | +| `titleTextColor` | `callerName` | +| `subtitleTextColor` | `subtitle` | +| `avatarStyle` background | `avatarBackground` (and `avatarText` for the initials) | +| `acceptButtonBackgroundColor` / `rejectButtonBackgroundColor` | `acceptButton` / `declineButton` | +| `acceptButtonTextColor` / `rejectButtonTextColor` | `acceptLabel` / `declineLabel` (the labels sit under the buttons) | +| `iconTint`, `voiceCallIcon`, `videoCallIcon` | none: the screen shows "Incoming audio call" text, not a call-type icon. The button icons are `acceptIcon` / `declineIcon` | +| `cornerRadius`, `strokeWidth`, `strokeColor` | none: the screen is full page, not a card | +| text appearances (font, size) | none yet: colours only | See [Component Styling](/ui-kit/android/component-styling) for the full reference.