# Custom call event handler on Android

A custom call event handler is your own Java class that receives VoIP call events on the native Android side. Use it to open your own call screen when the user answers, or to pass call events to a part of the app that the main WebView can't reach, such as a second activity with its own WebView.

The handler works in Cordova apps and in Capacitor apps that use `pushwoosh-cordova-plugin`. It requires plugin version 8.3.64 or later. Use 8.3.80 or later: since that version, JavaScript call events that arrive while the WebView is being recreated are delivered once JavaScript is ready.

Before you start, set up VoIP calls as described in [Cordova VoIP calls](/developer/pushwoosh-sdk/cross-platform-frameworks/cordova/voip-cordova/): enable `PW_VOIP_ANDROID_ENABLED` and request the call permission with `requestCallPermission()`. In a Capacitor app, set `PW_VOIP_ANDROID_ENABLED=true` in `android/gradle.properties`.

## How the handler works

- For every call event, the plugin first sends the JavaScript event to the main WebView, then calls your handler. Your `registerEvent` listeners keep working.
- When a handler is set, the plugin doesn't bring the app to the foreground on Answer. Without a handler, the plugin opens the app's launcher activity. With a handler, open your screen in `onAnswer`, or the app stays in the background.
- The plugin reads the class name from the app manifest on the first call event after the app process starts, and creates the class through reflection. The class needs a public constructor without arguments.
- All callbacks run on the main thread.
- The callbacks don't receive a `Context`. Keep an application `Context` in your app, as shown in [Step 2](#2-keep-an-application-context).
- If a callback throws an exception, the plugin logs it. JavaScript events are still delivered.

### Call events

The handler implements `com.pushwoosh.calls.listener.CallEventListener`. Implement all eight methods, and leave the ones you don't need empty.

| Method | Called when | JavaScript event |
|---|---|---|
| `onCreateIncomingConnection(Bundle payload)` | An incoming call starts ringing | `voipPushPayload` |
| `onAnswer(PushwooshVoIPMessage message, int videoState)` | The user answers | `answer` |
| `onReject(PushwooshVoIPMessage message)` | The user declines | `reject` |
| `onDisconnect(PushwooshVoIPMessage message)` | The call ends | `hangup` |
| `onCallCancelled(PushwooshVoIPMessage message)` | A cancel push cancels the call | `voipDidCancelCall` |
| `onCallCancellationFailed(String callId, String reason)` | A cancel push can't be applied | `voipDidFailToCancelCall` |
| `onCallAdded(PushwooshVoIPMessage message)` | The system in-call service adds the call | None |
| `onCallRemoved(PushwooshVoIPMessage message)` | The system in-call service removes the call | None |

`PushwooshVoIPMessage` gives you the values from the VoIP push:

| Getter | Returns |
|---|---|
| `getCallId()` | `callId`, or `null` if the push has none |
| `getCallerName()` | `callerName`, or `Unknown Caller` if the push has none |
| `getHasVideo()` | `video` |
| `getSupportsHolding()` | `supportsHolding` |
| `getSupportsDTMF()` | `supportsDTMF` |
| `getCancelCall()` | `cancelCall` |
| `getRawPayload()` | The whole push `Bundle`, including your custom keys |

`onCreateIncomingConnection` receives the raw `Bundle`. To use the getters there, wrap it: `new PushwooshVoIPMessage(payload)`.

## 1. Register the handler in the manifest

Add a `meta-data` entry to `<application>` with the full class name of your handler:

```xml
<application ...>
    <meta-data
        android:name="com.pushwoosh.cordova.CALL_EVENT_HANDLER"
        android:value="com.example.app.MyCallEventHandler" />
</application>
```

[Step 5](#5-add-the-code-to-your-project) shows where this entry goes in Cordova and in Capacitor.

## 2. Keep an application Context

The handler needs a `Context` to start an activity or send a broadcast. The VoIP push can start the app process itself, so store the `Context` in a place that's ready before the first call event.

In Capacitor, use a custom `Application` class:

```java
public class MyApp extends Application {
    private static Context context;

    public static Context context() {
        return context;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        context = getApplicationContext();
    }
}
```

```xml
<application android:name="com.example.app.MyApp" ...>
```

In Cordova, use a small `ContentProvider`. Android creates it when the app process starts:

```java
public class AppContextHolder extends ContentProvider {
    private static Context context;

    public static Context context() {
        return context;
    }

    @Override
    public boolean onCreate() {
        context = getContext().getApplicationContext();
        return true;
    }

    @Override public Cursor query(Uri u, String[] p, String s, String[] a, String o) { return null; }
    @Override public String getType(Uri uri) { return null; }
    @Override public Uri insert(Uri uri, ContentValues values) { return null; }
    @Override public int delete(Uri uri, String s, String[] a) { return 0; }
    @Override public int update(Uri uri, ContentValues v, String s, String[] a) { return 0; }
}
```

```xml
<provider
    android:name="com.example.app.AppContextHolder"
    android:authorities="${applicationId}.appcontext"
    android:exported="false" />
```

<Aside type="note">
Don't set a custom `Application` class from a Cordova plugin. It needs `<edit-config>` on `<application>`, and the Cordova CLI fails with "There was a conflict trying to modify attributes with `<edit-config>`" because the Pushwoosh plugin also adds entries to `<application>`.
</Aside>

## 3. Write the handler

This handler opens your call screen on Answer and sends each call event as a broadcast inside the app:

```java
public class MyCallEventHandler implements CallEventListener {
    public static final String ACTION_CALL_EVENT = "com.example.app.CALL_EVENT";

    @Override
    public void onAnswer(@NonNull PushwooshVoIPMessage message, int videoState) {
        Context context = MyApp.context(); // AppContextHolder.context() in Cordova

        Intent intent = new Intent(context, VideoCallActivity.class)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                        | Intent.FLAG_ACTIVITY_CLEAR_TOP
                        | Intent.FLAG_ACTIVITY_SINGLE_TOP)
                .putExtra("callId", message.getCallId());
        context.startActivity(intent);

        broadcast("answer", message);
    }

    @Override
    public void onReject(@NonNull PushwooshVoIPMessage message) {
        broadcast("reject", message);
    }

    @Override
    public void onDisconnect(@NonNull PushwooshVoIPMessage message) {
        broadcast("hangup", message);
    }

    @Override
    public void onCallCancelled(@NonNull PushwooshVoIPMessage message) {
        broadcast("cancelled", message);
    }

    @Override public void onCreateIncomingConnection(@Nullable Bundle payload) {}
    @Override public void onCallAdded(@NonNull PushwooshVoIPMessage message) {}
    @Override public void onCallRemoved(@NonNull PushwooshVoIPMessage message) {}
    @Override public void onCallCancellationFailed(@Nullable String callId, @NonNull String reason) {}

    private static void broadcast(String event, PushwooshVoIPMessage message) {
        Context context = MyApp.context(); // AppContextHolder.context() in Cordova
        Intent intent = new Intent(ACTION_CALL_EVENT)
                .setPackage(context.getPackageName())
                .putExtra("event", event)
                .putExtra("callId", message.getCallId());
        context.sendBroadcast(intent);
    }
}
```

Keep all three intent flags:

- `FLAG_ACTIVITY_NEW_TASK` is required to start an activity outside an activity. If the app's task exists, Android brings it to the front and opens the call screen on top. The main activity isn't recreated, even with `launchMode="singleTask"`.
- `FLAG_ACTIVITY_SINGLE_TOP` makes an open call screen receive the new call in `onNewIntent` instead of opening a second copy.
- `FLAG_ACTIVITY_CLEAR_TOP` covers the case when Android stopped the app process while the call screen was open. Without it, a second call screen opens on top of the old one.

Pass everything the call screen needs through intent extras. If the call screen isn't open yet, it misses the `answer` broadcast, because the broadcast is sent before the screen starts listening. Broadcasts only reach screens that are already open.

`setPackage()` keeps the broadcast inside your app.

## 4. Write the call screen

The call screen reads the call from its intent and listens for the broadcasts:

```java
public class VideoCallActivity extends Activity {

    private final BroadcastReceiver callEvents = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String event = intent.getStringExtra("event");
            String callId = intent.getStringExtra("callId");
            // Pass the event to your UI or WebView.
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Set up your layout or WebView.

        if (savedInstanceState == null) {
            joinCall(getIntent());
        }
        ContextCompat.registerReceiver(this, callEvents,
                new IntentFilter(MyCallEventHandler.ACTION_CALL_EVENT),
                ContextCompat.RECEIVER_NOT_EXPORTED);
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        joinCall(intent);
    }

    @Override
    protected void onDestroy() {
        unregisterReceiver(callEvents);
        super.onDestroy();
    }

    private void joinCall(Intent intent) {
        String callId = intent.getStringExtra("callId");
        // Join the call.
    }
}
```

```xml
<activity
    android:name="com.example.app.VideoCallActivity"
    android:exported="false" />
```

- Register the receiver in `onCreate` and unregister it in `onDestroy`, not in `onResume` and `onPause`. This way the call screen gets events such as `hangup` while the app is in the background.
- `RECEIVER_NOT_EXPORTED` keeps the receiver closed to other apps. Android 14 and later require every receiver registered at runtime to be marked as exported or not exported.
- Read `getIntent()` in `onCreate` only when `savedInstanceState` is `null`. If Android stopped the app process while the call screen was open, it recreates the screen with the intent of the previous call and then passes the new call to `onNewIntent`. Without the check, the screen tries to join the previous call first.

## 5. Add the code to your project

### Capacitor

1. Add the Java classes to `android/app/src/main/java/`.
2. In `android/app/src/main/AndroidManifest.xml`, register the handler `meta-data`, the call screen activity, and the `Application` class.
3. In `android/app/build.gradle`, add the Pushwoosh calls SDK to the app module. Use the version that the installed plugin uses, which is set in the plugin's `src/android/add-android-voip.gradle`:

   ```gradle
   dependencies {
       compileOnly "com.pushwoosh:pushwoosh-calls:6.17.1"
   }
   ```

   Without it, the build fails with `package com.pushwoosh.calls.listener does not exist`, because the plugin adds the calls SDK only to its own module.

### Cordova

Cordova regenerates the native project, so put the Java classes into a small local plugin:

```
my-call-handler/
  package.json
  plugin.xml
  src/android/AppContextHolder.java
  src/android/MyCallEventHandler.java
  src/android/VideoCallActivity.java
```

The plugin needs a `package.json`. Without it, `cordova plugin add` fails with "Invalid Plugin! … needs a valid package.json".

```json
{
  "name": "my-call-handler",
  "version": "1.0.0",
  "cordova": { "id": "my-call-handler", "platforms": ["android"] }
}
```

`plugin.xml` copies the classes and adds the provider, the activity, and the handler `meta-data` to the manifest:

```xml
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
        xmlns:android="http://schemas.android.com/apk/res/android"
        id="my-call-handler" version="1.0.0">
    <platform name="android">
        <source-file src="src/android/AppContextHolder.java" target-dir="src/com/example/app" />
        <source-file src="src/android/MyCallEventHandler.java" target-dir="src/com/example/app" />
        <source-file src="src/android/VideoCallActivity.java" target-dir="src/com/example/app" />

        <config-file target="AndroidManifest.xml" parent="/manifest/application">
            <provider android:name="com.example.app.AppContextHolder"
                      android:authorities="${applicationId}.appcontext"
                      android:exported="false" />
            <activity android:name="com.example.app.VideoCallActivity" android:exported="false" />
            <meta-data android:name="com.pushwoosh.cordova.CALL_EVENT_HANDLER"
                       android:value="com.example.app.MyCallEventHandler" />
        </config-file>
    </platform>
</plugin>
```

Install it:

```sh
cordova plugin add ./my-call-handler
```

Cordova doesn't need an extra Gradle dependency, because the calls SDK is already available to the app.

## When the app isn't running

If the user answers after the app was removed from Recents, the call screen opens as the only activity of a new task:

- The main activity and its WebView don't start, so pressing Back on the call screen closes the app.
- The JavaScript `answer` event waits in the plugin until the app's JavaScript starts.
- If the call screen needs something from the main app, such as the login state, load it in the call screen.

## Troubleshooting

The plugin writes these messages to logcat with the `PWCordovaCallEventListener` tag:

| Message | Meaning |
|---|---|
| `Custom CallEventHandler resolved: <class>` | The plugin found and created the handler. Appears on the first call event. |
| `Custom handler class not found (check ProGuard rules): …` | The class name in `meta-data` is wrong, or code shrinking removed the class. |
| `Custom handler missing default constructor: …` | The class has no public constructor without arguments. |
| `Failed to resolve custom CallEventHandler` | Any other loading error, for example the class doesn't implement `CallEventListener`. |
| `Custom handler onAnswer() threw exception` and similar | A handler callback threw an exception. JavaScript events are still delivered. |

If the plugin can't load the handler, it works as if no handler is set, including opening the app on Answer. It tries again only after the app process restarts.

With VoIP enabled, the plugin adds a ProGuard rule that keeps every class implementing `CallEventListener`, so code shrinking doesn't remove your handler.

## Related links

- [Cordova VoIP calls](/developer/pushwoosh-sdk/cross-platform-frameworks/cordova/voip-cordova/)
- [Android VoIP calls](/developer/pushwoosh-sdk/android-sdk/pushwoosh-android-voip-calls/)