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
registerEventlisteners 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 applicationContextin 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 toThe 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
Anchor link toAdd 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 toThe 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 toThis 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_TASKis 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 withlaunchMode="singleTask".FLAG_ACTIVITY_SINGLE_TOPmakes an open call screen receive the new call inonNewIntentinstead of opening a second copy.FLAG_ACTIVITY_CLEAR_TOPcovers 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 toThe 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
onCreateand unregister it inonDestroy, not inonResumeandonPause. This way the call screen gets events such ashangupwhile the app is in the background. RECEIVER_NOT_EXPORTEDkeeps 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()inonCreateonly whensavedInstanceStateisnull. 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 toonNewIntent. Without the check, the screen tries to join the previous call first.
5. Add the code to your project
Anchor link toCapacitor
Anchor link to-
Add the Java classes to
android/app/src/main/java/. -
In
android/app/src/main/AndroidManifest.xml, register the handlermeta-data, the call screen activity, and theApplicationclass. -
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’ssrc/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
Anchor link toCordova 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.javaThe 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:
cordova plugin add ./my-call-handlerCordova 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 toIf 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
answerevent 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 toThe 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.