Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
7b78ac3
CordovaCall.java: set PhoneAccount capability: CAPABILITY_SELF_MANAGED
WesUnwin Oct 23, 2025
ead13b0
Create PhoneAccountManager.java
WesUnwin Oct 23, 2025
5a5389a
MyConnectionService: add logic to receive intent, add incoming connec…
WesUnwin Oct 23, 2025
37421de
MyConnectionService: code cleanups
WesUnwin Oct 23, 2025
73e360d
Create CallNotification.java
WesUnwin Oct 24, 2025
c1d1c4e
MyConnectionService: create/show/close using CallNotification class
WesUnwin Oct 24, 2025
9642827
MyConnectionService: handle startup with no intent, add logging for i…
WesUnwin Oct 24, 2025
1ea8e69
CallNotifications: use CallStyle notifications for incoming call + fi…
WesUnwin Oct 24, 2025
bcc4c25
MyConnectionService: mark connection disconnected when receiving push…
WesUnwin Oct 24, 2025
b5aa4c7
MyConnectionService: improve/fix logging, dont addIncomingCall multip…
WesUnwin Oct 24, 2025
6e140f3
MyConnectionService: FIX: set connection property indicating its to b…
WesUnwin Oct 24, 2025
1d9a9c5
Updates
WesUnwin Oct 27, 2025
0734a99
Create CallActionReceiver registered with MyConnectionService to hand…
WesUnwin Oct 28, 2025
1767d91
MyConnectionService: DRY out disconnect logic, rename conn to activeC…
WesUnwin Oct 28, 2025
c3d6b47
Updates
WesUnwin Oct 28, 2025
ef99794
Fix comment about setOngoing
WesUnwin Oct 28, 2025
b58a923
plugin.xml declare CallActionReceiver in android manifest
WesUnwin Oct 28, 2025
a5c0b02
plugin.xml: declare newly created java files so they get installed wi…
WesUnwin Oct 28, 2025
409b697
plugin.xml: fix source-file target directory values
WesUnwin Oct 28, 2025
e933f2b
CordovaCall.java: code cleanups
WesUnwin Oct 28, 2025
ff29340
CordovaCall.java: more code cleanups
WesUnwin Oct 28, 2025
196993b
MyConnectionService: change up implementation of showWebApp()
WesUnwin Oct 28, 2025
ebb5625
Fix logic opening main activity, immediately close notification in Ca…
WesUnwin Oct 28, 2025
ec74e5e
Code cleanups
WesUnwin Oct 28, 2025
6f78052
CordovaCall: cleanup and fix initialization of callbackContextMap
WesUnwin Oct 28, 2025
2e194d7
Revert "CordovaCall: cleanup and fix initialization of callbackContex…
WesUnwin Oct 28, 2025
301ebfd
CordovaCall: callbackContextMap fixes
WesUnwin Oct 28, 2025
574b003
CordovaCall: add logging to emitEvent, add callbackContext array list…
WesUnwin Oct 28, 2025
1f1a08a
Fix conflicts merging other PR
WesUnwin Oct 30, 2025
3589721
MyConnectionService.java: remove delay before emitting reject event
WesUnwin Oct 30, 2025
49b8e84
CallNotification.java: remove logic to create notification channel - …
WesUnwin Oct 30, 2025
3183410
MyCOnnectionService.java: FIX: disonnectConnection() remove from conn…
WesUnwin Oct 30, 2025
c10dced
More improvements to connection cleanup
WesUnwin Oct 30, 2025
775f692
MyConnectionService: improve comment about userAction in intent used …
WesUnwin Oct 30, 2025
f1fbaa1
CallNotification: fix Full Screen Intent (now actually launches main …
WesUnwin Oct 30, 2025
e013e37
CordovaCall: add logic on plugin initialize to invoke setShowWhenLock…
WesUnwin Oct 30, 2025
05c44f1
Create IncomingCallActivity for full screen intent (for lock screen p…
WesUnwin Oct 30, 2025
ef454f6
CordovaCall: ensure PhoneAccount is created + registered before refer…
WesUnwin Oct 30, 2025
879190b
plugin.xml: add source-file directive for IncomingCallActivity.java
WesUnwin Oct 30, 2025
77250c2
Delete unused import statements across src/android
WesUnwin Oct 30, 2025
e265483
MyConnectionService: unregister callActionReceiver in onDestroy (remo…
WesUnwin Oct 30, 2025
457c44d
MyConnectionService: remove some blank lines
WesUnwin Oct 30, 2025
837e804
Add temporary code to end existing lingering calls (till problems the…
WesUnwin Oct 31, 2025
0a1e86c
CordovaCall.java: dont clear array each plugin init (arrays are stati…
WesUnwin Oct 31, 2025
f8064fa
CallNotification.java: create custom incoming_calls notification chan…
WesUnwin Oct 31, 2025
5aa6913
CallNotification: fix remove old reference to ringtone
WesUnwin Oct 31, 2025
9579b4a
CallNotification: set ringtone on both channel and notification (for …
WesUnwin Oct 31, 2025
bc3cc32
Fix conflicts
WesUnwin Oct 31, 2025
2945e41
IncomingCallActivity: reuse CallActionReceiver, remove bad notificati…
WesUnwin Oct 31, 2025
aafe0e1
MyConnectionService.java: FIX: close notification onAnswer, improve l…
WesUnwin Oct 31, 2025
7bbf13d
Slight improvements to disconnectConnection() and lingering call cleanup
WesUnwin Oct 31, 2025
3370118
MyConnectionService: delete unused from intent extra
WesUnwin Oct 31, 2025
108498a
CallNotification.java: add comment about creating channel in plugin
WesUnwin Oct 31, 2025
f84ba3d
CallNotification.java: improve notification channel description
WesUnwin Oct 31, 2025
eb02b20
IncomingCallActivity: FIX bug where old pushMessagePaylaod was cached…
WesUnwin Oct 31, 2025
129a163
CallNotification.java: remove unused intent extras
WesUnwin Oct 31, 2025
53738a4
MyConnectionService: add line of code to remove/cleanup entries in co…
WesUnwin Nov 3, 2025
2ff1c3d
CallActionReceiver: For safety/robustness add logic to close orphaned…
WesUnwin Nov 3, 2025
c6eeb94
Fix: check activeConnectionUUID for null in disconnectConection()
WesUnwin Nov 4, 2025
cdecaad
MyConnectionService: declare connectionMap and connectionAddedMap as …
WesUnwin Nov 4, 2025
87798de
MyConnectionService.java: handle case where intent.getAction() may be…
WesUnwin Nov 4, 2025
990deef
MyConnectionService: use connection.onStateChanged to improve/simplif…
WesUnwin Nov 4, 2025
4c9bbf5
IncomingCallActivity: add logic to finish/destroy activity if corresp…
WesUnwin Nov 4, 2025
9f99c2e
Remove (now not needed) temp code to kill lingering connections
WesUnwin Nov 4, 2025
38977f7
plugin.xml: remove uses-permission declarations for READ_PHONE_STATE,…
WesUnwin Nov 4, 2025
5c8e486
Add back android manifest uses-permission for READ_PHONE_NUMBERS
WesUnwin Nov 4, 2025
c3968ff
CordovaCall.java: add comment indicating that tm.getPhoneAccount requ…
WesUnwin Nov 4, 2025
f5664e6
Create CordovaCall plugin functions to check canUseFullScreenIntent()…
WesUnwin Nov 6, 2025
aa106df
Create CordovaCall plugin functions to check canUseFullScreenIntent()…
WesUnwin Nov 6, 2025
afacbdd
Create CordovaCall plugin functions to check canUseFullScreenIntent()…
WesUnwin Nov 6, 2025
63f23d6
Change API to openFullScreenIntentSettings
WesUnwin Nov 7, 2025
dcc6aa0
Fix return value for canUseFullScreenIntent
WesUnwin Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,35 @@
<config-file parent="/*" target="AndroidManifest.xml">
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/> <!-- needed by TelecomManager.getPhoneAccount() -->
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
</config-file>

<config-file parent="/manifest/application" target="AndroidManifest.xml">
<activity
android:name="com.dmarc.cordovacall.IncomingCallActivity"
android:showOnLockScreen="true"
android:turnScreenOn="true"
android:launchMode="singleTask"
android:exported="false"
android:theme="@style/Theme.AppCompat.NoActionBar"
/>
<service android:name="com.dmarc.cordovacall.MyConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
<receiver android:name="com.dmarc.cordovacall.CallActionReceiver" />
</config-file>

<source-file src="src/android/CallActionReceiver.java" target-dir="src/com/dmarc/cordovacall" />
<source-file src="src/android/CallNotification.java" target-dir="src/com/dmarc/cordovacall" />
<source-file src="src/android/CordovaCall.java" target-dir="src/com/dmarc/cordovacall" />
<source-file src="src/android/IncomingCallActivity.java" target-dir="src/com/dmarc/cordovacall" />
<source-file src="src/android/MyConnectionService.java" target-dir="src/com/dmarc/cordovacall" />
<source-file src="src/android/PhoneAccountManager.java" target-dir="src/com/dmarc/cordovacall" />
</platform>

<platform name="ios">
Expand Down
41 changes: 41 additions & 0 deletions src/android/CallActionReceiver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.dmarc.cordovacall;

import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telecom.Connection;
import android.util.Log;

public class CallActionReceiver extends BroadcastReceiver {
static final String TAG = "CallActionReceiver";

@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.d(TAG, "onReceive, intent action: " + action);
String pushMessagePayload = intent.getStringExtra("pushMessagePayload");

Connection conn = MyConnectionService.getConnectionByPayload(pushMessagePayload);

if (conn != null) {
if (action.equals("declineCall")) {
conn.onReject();
} else if (action.equals("answerCall")) {
conn.onAnswer();
} else {
throw new RuntimeException("Invalid action: " + action);
}
} else {
Log.d(TAG, "Exiting, connection no longer exists. pushMessagePayload: " + pushMessagePayload);
if (intent.hasExtra("notificationID")) {
// For safety ensure any associated notification is closed (avoids bad UX if the normal logic
// that closes the notification when the connection is disconnected fails/crashes/etc.)
int notificationID = intent.getIntExtra("notificationID", 0);
Log.e(TAG, "Closing orphaned notification, ID: " + notificationID);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(notificationID);
}
}
}
}
153 changes: 153 additions & 0 deletions src/android/CallNotification.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.dmarc.cordovacall;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.util.Log;

import androidx.core.app.NotificationCompat;
import androidx.core.app.Person;
import org.json.JSONException;
import org.json.JSONObject;

import java.net.URI;
import java.util.Random;


public class CallNotification {
private static final String TAG = "CallNotification";

private String pushMessagePayload;
private Integer notificationID;
private Context context;
private NotificationManager notificationManager;
private Runnable timeoutRunnable;
private Handler timeoutHandler = new Handler();

private static final String NOTIFICATION_CHANNEL_ID = "incoming_calls";

public CallNotification(String pushMessagePayload, Context context) {
this.pushMessagePayload = pushMessagePayload;
this.notificationID = new Random().nextInt(100000) + 1; // Random int > 0 TODO: maybe derive from call UUID string
this.context = context;
this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

this.createNotificationChannel();
}

public void show() {
int timeout = 30000;

// NOTE: "Notifications should only launch a BroadcastReceiver from notification actions"

Intent answerIntent = new Intent(this.context, CallActionReceiver.class);
answerIntent.setAction("answerCall");
answerIntent.putExtra("pushMessagePayload", this.pushMessagePayload);
answerIntent.putExtra("notificationID", this.notificationID);
PendingIntent answerPendingIntent = PendingIntent.getBroadcast(
this.context, 0, answerIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);

Intent declineIntent = new Intent(this.context, CallActionReceiver.class);
declineIntent.setAction("declineCall");
declineIntent.putExtra("pushMessagePayload", this.pushMessagePayload);
declineIntent.putExtra("notificationID", this.notificationID);
PendingIntent declinePendingIntent = PendingIntent.getBroadcast(
this.context, 1, declineIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);

JSONObject payload = null;
try {
payload = new JSONObject(this.pushMessagePayload);
} catch (JSONException e) {
throw new RuntimeException("CallNotification unable to parse pushMessagePayload, error: " + e);
}

String callerName = payload.optString("from", "UNKNOWN");

NotificationCompat.Builder builder = new NotificationCompat.Builder(this.context, CallNotification.NOTIFICATION_CHANNEL_ID)
.setContentTitle("Incoming call")
.setSmallIcon(android.R.drawable.ic_menu_call)
.setLargeIcon(BitmapFactory.decodeResource(this.context.getResources(), android.R.drawable.sym_def_app_icon))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setSound(this.getRingtoneURI()) // For compatibility with Android 8.0 and less. (normally set through channel)
.setOngoing(true); // Can't be "dismissed" by the user, app will handle closing it

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Log.d(TAG, "Creating CallStyle.forIncomingCall style notification (as this is supported by the device)...");
Person callerPerson = new Person.Builder()
.setName(callerName)
.setImportant(true)
.build();

// "CallStyle notifications must be for a foreground service or user initated job or use a fullScreenIntent."
Intent fullScreenIntent = new Intent(this.context, IncomingCallActivity.class);
fullScreenIntent.putExtra("pushMessagePayload", this.pushMessagePayload);
PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(
this.context, 0, fullScreenIntent,
PendingIntent.FLAG_IMMUTABLE
);

builder.setStyle(NotificationCompat.CallStyle.forIncomingCall(callerPerson, declinePendingIntent, answerPendingIntent));
builder.setFullScreenIntent(fullScreenPendingIntent, true);
} else {
builder.setContentText(callerName);
builder.addAction(android.R.drawable.ic_menu_call, "Answer", answerPendingIntent)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent);
}

Log.d(TAG, "launching call notification via android NotificationManager notify()...");
notificationManager.notify(this.notificationID, builder.build());

if (this.timeoutRunnable != null) {
this.timeoutHandler.removeCallbacks(this.timeoutRunnable);
}

this.timeoutRunnable = new Runnable() {
@Override
public void run() {
close();
}
};

this.timeoutHandler.postDelayed(timeoutRunnable, timeout);
}

public void close() {
Log.d(TAG, "closing call notification");
this.notificationManager.cancel(this.notificationID);
if (this.timeoutRunnable != null) {
this.timeoutHandler.removeCallbacks(this.timeoutRunnable);
}
}

private Uri getRingtoneURI() {
return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
}

// Intentional create the notification channel here (rather than in javascript via window.FirebasePlugin.createChannel)
// so that the users default ringtone can be referenced, and so that the channel is guaranteed to be established prior to the notification.
// The notification sound on > Android 8.0 comes from the notification channel.
private void createNotificationChannel() {
NotificationChannel channel = new NotificationChannel(
CallNotification.NOTIFICATION_CHANNEL_ID,
"Incoming Calls",
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription("Incoming call alerts");
channel.setSound(this.getRingtoneURI(), null);
channel.setVibrationPattern(new long[]{ 0, 1000, 500, 1000 });
channel.enableVibration(true);
this.notificationManager.createNotificationChannel(channel);
}
}
Loading