Skip to content

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: 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

Anchor link to
  • 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.
  • If a callback throws an exception, the plugin logs it. JavaScript events are still delivered.

Call events

Anchor link to

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

MethodCalled whenJavaScript event
onCreateIncomingConnection(Bundle payload)An incoming call starts ringingvoipPushPayload
onAnswer(PushwooshVoIPMessage message, int videoState)The user answersanswer
onReject(PushwooshVoIPMessage message)The user declinesreject
onDisconnect(PushwooshVoIPMessage message)The call endshangup
onCallCancelled(PushwooshVoIPMessage message)A cancel push cancels the callvoipDidCancelCall
onCallCancellationFailed(String callId, String reason)A cancel push can’t be appliedvoipDidFailToCancelCall
onCallAdded(PushwooshVoIPMessage message)The system in-call service adds the callNone
onCallRemoved(PushwooshVoIPMessage message)The system in-call service removes the callNone

PushwooshVoIPMessage gives you the values from the VoIP push:

GetterReturns
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

Anchor link to

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

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

Step 5 shows where this entry goes in Cordova and in Capacitor.

2. Keep an application Context

Anchor link to

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:

public class MyApp extends Application {
private static Context context;
public static Context context() {
return context;
}
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
}
<application android:name="com.example.app.MyApp" ...>

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

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; }
}
<provider
android:name="com.example.app.AppContextHolder"
android:authorities="${applicationId}.appcontext"
android:exported="false" />

3. Write the handler

Anchor link to

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

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

Anchor link to

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

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.
}
}
<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

Anchor link to
  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:

    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 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”.

{
"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:

<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:

Terminal window
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

Anchor link to

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

Anchor link to

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

MessageMeaning
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 CallEventHandlerAny other loading error, for example the class doesn’t implement CallEventListener.
Custom handler onAnswer() threw exception and similarA 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.

Anchor link to