> ## Documentation Index
> Fetch the complete documentation index at: https://www.cometchat.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating from Legacy Ringing

> 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 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](https://www.cometchat.com/docs/calls/android/ringing.md); for the push side see [Push ringing](https://www.cometchat.com/docs/calls/android/voip-calling.md).

## Before you start

Ringing with the Calls SDK is opt-in on the client: moving your app to the Calls SDK 5.1 and Push Notifications SDK 2.0 APIs below is the migration. While some of your users are still on legacy ringing, one dashboard setting lets the two flows call each other; see [While some users are still on legacy ringing](#while-some-users-are-still-on-legacy-ringing).

## 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 theme={null}
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 theme={null}
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 theme={null}
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.
* `compileSdk` 36 and Android Gradle Plugin 8.9.1 or later: the Push Notifications SDK 2.0 depends on `androidx.core` 1.18, which requires them.
* Keep your Firebase Messaging dependency and `google-services.json`; the Push SDK never compiles against Firebase.

Adding the `push-notifications-calls-android` artifact turns ringing on. There is no second initialiser.

## Step 2: Replace the push configuration

> [tab] Before (Push SDK 1.0)
>
> ```kotlin theme={null}
> 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 */)
>     }
> })
> ```

> [tab] After (Push SDK 2.0)
>
> ```kotlin theme={null}
> 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())
> ```

## Step 3: Shrink the Firebase service

> [tab] Before
>
> ```kotlin theme={null}
> override fun onMessageReceived(message: RemoteMessage) {
>     if (CometChatPushNotifications.isAppInForeground() && isCallPush(message.data)) {
>         return   // the WebSocket CallListener handles it
>     }
>     CometChatPushNotifications.handlePushNotification(this, title, body, icon, uid, guid, message.data)
> }
>
> override fun onNewToken(token: String) {
>     CometChatPushNotifications.handleTokenRefresh(PushPlatform.FCM_ANDROID, token, PROVIDER_ID)
> }
> ```

> [tab] After
>
> ```kotlin theme={null}
> 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 delivers incoming call invitations to your app; the push is the only signal that a call is coming in, 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

## 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 your own `READ_PHONE_STATE` and `ANSWER_PHONE_CALLS` requests. (`MANAGE_OWN_CALLS` still merges in from the Calls SDK's manifest; leave it.)
* Any `isAppInForeground()` gate around call pushes

`RECORD_AUDIO`, `CAMERA` and `POST_NOTIFICATIONS` merge into your manifest from the Calls SDK and the Push SDK's calls module, so you can remove your own declarations; keep requesting them at runtime.

## Step 6: Rewrite the call code

| Legacy | Now | Notes |
| - | - | - |
| `CometChat.initiateCall(Call(uid, RECEIVER_TYPE_USER, type), listener)` | `CometChatCalls.initiateCall(context, uid, InitiateCallOptions, listener<InitiatedCall>)` | Set `options.sessionSettings` with the session type, or an audio call opens the camera, and with `setPeerCall(true)` for a 1:1 call |
| `CometChat.initiateCall(call, timeoutSeconds, listener)` | No client timeout. The callee's device rings for 45 s (or the push's `ringTimeout`) and then ends the call as `unanswered` | Run your own 45 s timer on the caller: if the callee's device is never reached, nothing else ends the caller's ringing for 5 minutes |
| `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<Void>)` | |
| `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](https://www.cometchat.com/docs/calls/android/migration-guide-v5.md)) | |
| `CometChatCallsConstants.*` | `com.cometchat.calls.constants.Constants.*` | The whole `CometChatCallsConstants` class has been deprecated since 5.0 |
| `CometChat.logout(...)` | `CometChatCalls.logout(...)` as well | Otherwise the Calls SDK stays signed in as the old user and a ring is not stopped |

### Caller screen, before and after

> [tab] Before
>
> ```kotlin theme={null}
> 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)
> }
> ```

> [tab] After
>
> ```kotlin theme={null}
> 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

> [tab] Before
>
> ```kotlin theme={null}
> // Launched by your CallListener / ConnectionService
> acceptButton.setOnClickListener {
>     CometChat.acceptCall(sessionId, object : CometChat.CallbackListener<Call>() {
>         override fun onSuccess(call: Call) = joinCallSession(call.sessionId)
>         override fun onError(e: CometChatException) = finish()
>     })
> }
> declineButton.setOnClickListener {
>     CometChat.rejectCall(sessionId, CometChatConstants.CALL_STATUS_REJECTED, callback)
> }
> ```

> [tab] After
>
> ```kotlin theme={null}
> // 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 theme={null}
CometChat.logout(chatCallback)
CometChatCalls.logout(callsCallback)   // new: stops a ringing call, unregisters the push token
```

## 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, 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](https://www.cometchat.com/docs/calls/android/voip-calling.md) page.

## While some users are still on legacy ringing

Your users don't all update at once, so during the rollout one CometChat app has devices on both flows. A device's flow comes from its push registration: a device on the Calls SDK 5.1 and Push Notifications SDK 2.0 receives only the call-initiated push, and a device still on legacy ringing keeps receiving the legacy call events.

To let users on the two flows call each other, turn on **Connect legacy ringing** in the CometChat dashboard under **Voice & Video Calls → Settings**. CometChat then also sends calls made on this flow as legacy call events, so devices on either flow ring for calls from the other.

* Turn it on before the first users update, and leave it on while any version of your apps still in use rings the legacy way.
* It can't be turned off from the dashboard.
* The six legacy call events (ongoing, cancelled, rejected, unanswered, busy and ended) are under **Show legacy** in **Notifications → Settings → Call Preferences**. They only reach devices on legacy ringing.

## What users will notice

* Calls ring with the app killed, from the lock screen, without a `ConnectionService`.
* The caller sees "Ringing…" once the callee's phone is reached, and the reason when a call is declined or missed.
* The callee stops ringing after 45 seconds and the caller's screen closes with it (and on its own timer, if the callee's device was never reached).
* 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`
* [ ] 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
* [ ] **Connect legacy ringing** is on in the dashboard if some users stay on legacy ringing during the rollout
* [ ] Tested: ring with the app killed, decline, cancel, no answer, call during a call, logout while ringing


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.