rns-nativecall 0.9.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -49,8 +49,8 @@ CallHandler.registerHeadlessTask(async (data, eventType) => {
|
|
|
49
49
|
|
|
50
50
|
AppRegistry.registerComponent('main', () => App);
|
|
51
51
|
```
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
## Handling Events (Index.js)
|
|
53
|
+
|
|
54
54
|
```javascript
|
|
55
55
|
// Index.js Setup Foreground Listeners
|
|
56
56
|
CallHandler.subscribe(
|
|
@@ -79,8 +79,7 @@ CallHandler.subscribe(
|
|
|
79
79
|
|
|
80
80
|
````
|
|
81
81
|
|
|
82
|
-
## App.js
|
|
83
|
-
|
|
82
|
+
## Handling Events (App.js)
|
|
84
83
|
|
|
85
84
|
```javascript
|
|
86
85
|
import React, { useEffect } from 'react';
|
|
@@ -112,15 +111,129 @@ export default function App() {
|
|
|
112
111
|
```
|
|
113
112
|
---
|
|
114
113
|
### 📖 API Reference
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
| **
|
|
120
|
-
| **
|
|
121
|
-
| **
|
|
122
|
-
| **
|
|
114
|
+
# rns-nativecall API Reference
|
|
115
|
+
|
|
116
|
+
| Method | Platform | Description |
|
|
117
|
+
| :--- | :--- | :--- |
|
|
118
|
+
| **registerHeadlessTask(callback)** | Android | Registers background logic. `eventType` is `INCOMING_CALL` or `BUSY`. |
|
|
119
|
+
| **checkOverlayPermission()** | Android | Returns true if the app can draw over other apps (Required for Android Lockscreen). |
|
|
120
|
+
| **requestOverlayPermission()** | Android | Opens System Settings to let the user enable "Draw over other apps". |
|
|
121
|
+
| **displayCall(uuid, name, type)** | All | Shows the native call UI (Standard Notification on Android / CallKit on iOS). |
|
|
122
|
+
| **checkCallValidity(uuid)** | All | Returns boolean values for `isValid` and `isCanceled`. |
|
|
123
|
+
| **stopForegroundService()** | Android | Stops the ongoing service and clears the persistent notification/pill. |
|
|
124
|
+
| **destroyNativeCallUI(uuid)** | All | Dismisses the native call interface and stops the ringtone. |
|
|
125
|
+
| **getInitialCallData()** | All | Returns call data if the app was launched by clicking `Answer` from a killed state. |
|
|
126
|
+
| **subscribe(onAccept, onReject)** | All | Listens for native button presses (Answer/End). |
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
# Implementation Notes
|
|
131
|
+
|
|
132
|
+
1. Android Persistence:
|
|
133
|
+
Because this library uses a Foreground Service on Android, the notification will persist and show a "Call Pill" in the status bar. To remove this after the call ends or connects, you MUST call 'CallHandler.stopForegroundService()'.
|
|
134
|
+
|
|
135
|
+
2. Android Overlay:
|
|
136
|
+
For your React Native call screen to show up when the phone is locked, the user must grant the "Overlay Permission". Use 'checkOverlayPermission()' and 'requestOverlayPermission()' during your app's onboarding or call initiation.
|
|
137
|
+
|
|
138
|
+
3. iOS CallKit:
|
|
139
|
+
On iOS, 'displayCall' uses the native system CallKit UI. This works automatically in the background and on the lockscreen without extra overlay permissions.
|
|
140
|
+
|
|
141
|
+
4. Single Call Gate:
|
|
142
|
+
The library automatically prevents multiple overlapping native UIs. If a call is already active, subsequent calls will trigger the 'BUSY' event in your Headless Task.
|
|
123
143
|
---
|
|
124
144
|
|
|
145
|
+
## FULL Example Use Case
|
|
146
|
+
```javascript
|
|
147
|
+
import React, { useEffect, useState } from 'react';
|
|
148
|
+
import { StyleSheet, Text, View, TouchableOpacity, Alert } from 'react-native';
|
|
149
|
+
import { CallHandler } from 'rns-nativecall';
|
|
150
|
+
|
|
151
|
+
export default function App() {
|
|
152
|
+
const [activeCall, setActiveCall] = useState(null);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
// 1. Handle app launched from a notification "Answer" click
|
|
156
|
+
CallHandler.getInitialCallData().then((data) => {
|
|
157
|
+
if (data && data.default) {
|
|
158
|
+
console.log("App launched from call:", data.default);
|
|
159
|
+
setActiveCall(data.default);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// 2. Subscribe to foreground events
|
|
164
|
+
const unsubscribe = CallHandler.subscribe(
|
|
165
|
+
(data) => {
|
|
166
|
+
console.log("Call Accepted:", data.callUuid);
|
|
167
|
+
setActiveCall(data);
|
|
168
|
+
// Logic: Open your Video/Audio Call UI here
|
|
169
|
+
},
|
|
170
|
+
(data) => {
|
|
171
|
+
console.log("Call Rejected/Ended:", data.callUuid);
|
|
172
|
+
setActiveCall(null);
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return () => unsubscribe();
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
const startTestCall = async () => {
|
|
180
|
+
// Android Only: Check for overlay permission to show UI over lockscreen
|
|
181
|
+
const hasPermission = await CallHandler.checkOverlayPermission();
|
|
182
|
+
if (!hasPermission) {
|
|
183
|
+
Alert.alert(
|
|
184
|
+
"Permission Required",
|
|
185
|
+
"Please enable 'Draw over other apps' to see calls while the phone is locked.",
|
|
186
|
+
[
|
|
187
|
+
{ text: "Cancel" },
|
|
188
|
+
{ text: "Settings", onPress: () => CallHandler.requestOverlayPermission() }
|
|
189
|
+
]
|
|
190
|
+
);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Trigger the native UI
|
|
195
|
+
CallHandler.displayCall(
|
|
196
|
+
"test-uuid-" + Date.now(),
|
|
197
|
+
"John Doe",
|
|
198
|
+
"video"
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const endCallManually = () => {
|
|
203
|
+
if (activeCall) {
|
|
204
|
+
CallHandler.stopForegroundService();
|
|
205
|
+
setActiveCall(null);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<View style={styles.container}>
|
|
211
|
+
<Text style={styles.title}>RNS Native Call Pro</Text>
|
|
212
|
+
|
|
213
|
+
{activeCall ? (
|
|
214
|
+
<View style={styles.callBox}>
|
|
215
|
+
<Text>Active Call with: {activeCall.name}</Text>
|
|
216
|
+
<TouchableOpacity style={styles.btnEnd} onPress={endCallManually}>
|
|
217
|
+
<Text style={styles.btnText}>End Call</Text>
|
|
218
|
+
</TouchableOpacity>
|
|
219
|
+
</View>
|
|
220
|
+
) : (
|
|
221
|
+
<TouchableOpacity style={styles.btnStart} onPress={startTestCall}>
|
|
222
|
+
<Text style={styles.btnText}>Simulate Incoming Call</Text>
|
|
223
|
+
</TouchableOpacity>
|
|
224
|
+
)}
|
|
225
|
+
</View>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const styles = StyleSheet.create({
|
|
230
|
+
container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF' },
|
|
231
|
+
title: { fontSize: 20, fontWeight: 'bold', marginBottom: 20 },
|
|
232
|
+
callBox: { padding: 20, backgroundColor: '#e1f5fe', borderRadius: 10, alignItems: 'center' },
|
|
233
|
+
btnStart: { backgroundColor: '#4CAF50', padding: 15, borderRadius: 5 },
|
|
234
|
+
btnEnd: { backgroundColor: '#F44336', padding: 15, borderRadius: 5, marginTop: 10 },
|
|
235
|
+
btnText: { color: 'white', fontWeight: 'bold' }
|
|
236
|
+
});
|
|
237
|
+
```
|
|
125
238
|
## 🛡 License
|
|
126
239
|
---
|
|
@@ -52,6 +52,9 @@ class CallForegroundService : Service() {
|
|
|
52
52
|
): Int {
|
|
53
53
|
val data = intent?.extras
|
|
54
54
|
val name = data?.getString("name") ?: "Someone"
|
|
55
|
+
val uuid = data?.getString("callUuid") ?: "default_uuid"
|
|
56
|
+
|
|
57
|
+
val notificationId = uuid.hashCode()
|
|
55
58
|
|
|
56
59
|
createNotificationChannel()
|
|
57
60
|
|
|
@@ -68,12 +71,12 @@ class CallForegroundService : Service() {
|
|
|
68
71
|
|
|
69
72
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
70
73
|
startForeground(
|
|
71
|
-
|
|
74
|
+
notificationId,
|
|
72
75
|
notification,
|
|
73
76
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL,
|
|
74
77
|
)
|
|
75
78
|
} else {
|
|
76
|
-
startForeground(
|
|
79
|
+
startForeground(notificationId, notification)
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
// Launch the Headless Task
|
|
@@ -95,26 +98,13 @@ class CallForegroundService : Service() {
|
|
|
95
98
|
e.printStackTrace()
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
// // Trigger the incoming call notification UI handled by NativeCallManager
|
|
99
|
-
// try {
|
|
100
|
-
// val map = mutableMapOf<String, String>()
|
|
101
|
-
// data?.let { b ->
|
|
102
|
-
// for (key in b.keySet()) {
|
|
103
|
-
// map[key] = b.get(key)?.toString() ?: ""
|
|
104
|
-
// }
|
|
105
|
-
// }
|
|
106
|
-
// NativeCallManager.handleIncomingPush(this, map)
|
|
107
|
-
// } catch (e: Exception) {
|
|
108
|
-
// e.printStackTrace()
|
|
109
|
-
// }
|
|
110
|
-
|
|
111
101
|
// Auto-stop after 30s
|
|
112
|
-
Handler(Looper.getMainLooper()).postDelayed({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}, 30000)
|
|
102
|
+
// Handler(Looper.getMainLooper()).postDelayed({
|
|
103
|
+
// try {
|
|
104
|
+
// stopSelf()
|
|
105
|
+
// } catch (e: Exception) {
|
|
106
|
+
// }
|
|
107
|
+
// }, 30000)
|
|
118
108
|
|
|
119
109
|
return START_NOT_STICKY
|
|
120
110
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
3
|
import android.app.ActivityManager
|
|
4
|
-
import android.app.NotificationManager
|
|
5
4
|
import android.app.NotificationChannel
|
|
5
|
+
import android.app.NotificationManager
|
|
6
6
|
import android.app.PendingIntent
|
|
7
7
|
import android.content.Context
|
|
8
8
|
import android.content.Intent
|
|
@@ -10,14 +10,12 @@ import android.os.Build
|
|
|
10
10
|
import android.os.Bundle
|
|
11
11
|
import androidx.core.app.NotificationCompat
|
|
12
12
|
import androidx.core.content.ContextCompat
|
|
13
|
+
import com.facebook.react.HeadlessJsTaskService
|
|
13
14
|
import com.google.firebase.messaging.FirebaseMessagingService
|
|
14
15
|
import com.google.firebase.messaging.RemoteMessage
|
|
15
|
-
import com.facebook.react.HeadlessJsTaskService
|
|
16
|
-
|
|
17
16
|
import com.rnsnativecall.CallState
|
|
18
17
|
|
|
19
18
|
class CallMessagingService : FirebaseMessagingService() {
|
|
20
|
-
|
|
21
19
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
|
22
20
|
val data = remoteMessage.data
|
|
23
21
|
val context = applicationContext
|
|
@@ -26,78 +24,62 @@ class CallMessagingService : FirebaseMessagingService() {
|
|
|
26
24
|
val type = data["type"] ?: ""
|
|
27
25
|
|
|
28
26
|
if (type == "CANCEL") {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (CallState.getCurrent() == uuid) {
|
|
34
|
-
CallState.clear(uuid, context)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
NativeCallManager.dismissIncomingCall(context, uuid)
|
|
38
|
-
showMissedCallNotification(context, data, uuid)
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Inside onMessageReceived
|
|
43
|
-
if (!CallState.setCurrent(uuid)) {
|
|
44
|
-
// We are busy! Start a SILENT headless task to send the WebSocket busy msg
|
|
45
|
-
val busyIntent = Intent(context, CallHeadlessTask::class.java).apply {
|
|
46
|
-
putExtras(Bundle().apply {
|
|
47
|
-
data.forEach { (k, v) -> putString(k, v) }
|
|
48
|
-
putBoolean("isBusySignal", true)
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
context.startService(busyIntent)
|
|
52
|
-
return
|
|
53
|
-
}
|
|
27
|
+
NativeCallManager.stopRingtone()
|
|
28
|
+
// Pass context here to persist the cancellation
|
|
29
|
+
CallState.markCanceled(uuid, context)
|
|
54
30
|
|
|
31
|
+
if (CallState.getCurrent() == uuid) {
|
|
32
|
+
CallState.clear(uuid, context)
|
|
33
|
+
}
|
|
55
34
|
|
|
35
|
+
NativeCallManager.dismissIncomingCall(context, uuid)
|
|
36
|
+
showMissedCallNotification(context, data, uuid)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
56
39
|
|
|
40
|
+
// Inside onMessageReceived
|
|
41
|
+
if (!CallState.setCurrent(uuid)) {
|
|
42
|
+
// We are busy! Start a SILENT headless task to send the WebSocket busy msg
|
|
43
|
+
val busyIntent =
|
|
44
|
+
Intent(context, CallHeadlessTask::class.java).apply {
|
|
45
|
+
putExtras(
|
|
46
|
+
Bundle().apply {
|
|
47
|
+
data.forEach { (k, v) -> putString(k, v) }
|
|
48
|
+
putBoolean("isBusySignal", true)
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
context.startService(busyIntent)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
57
55
|
|
|
58
56
|
if (isAppInForeground(context)) {
|
|
59
57
|
// Foreground → send event directly
|
|
60
58
|
CallModule.sendEventToJS("onCallReceived", data)
|
|
61
59
|
} else {
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// }
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// Background → start foreground service (which in turn starts headless)
|
|
82
|
-
val serviceIntent = Intent(context, CallForegroundService::class.java).apply {
|
|
83
|
-
putExtras(Bundle().apply {
|
|
84
|
-
data.forEach { (k, v) -> putString(k, v) }
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
89
|
-
context.startForegroundService(serviceIntent)
|
|
90
|
-
} else {
|
|
91
|
-
context.startService(serviceIntent)
|
|
92
|
-
}
|
|
93
|
-
|
|
60
|
+
// Background → start foreground service (which in turn starts headless)
|
|
61
|
+
val serviceIntent =
|
|
62
|
+
Intent(context, CallForegroundService::class.java).apply {
|
|
63
|
+
putExtra("callUuid", uuid) // Key: Pass the UUID here
|
|
64
|
+
putExtras(
|
|
65
|
+
Bundle().apply {
|
|
66
|
+
data.forEach { (k, v) -> putString(k, v) }
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
72
|
+
context.startForegroundService(serviceIntent)
|
|
73
|
+
} else {
|
|
74
|
+
context.startService(serviceIntent)
|
|
75
|
+
}
|
|
94
76
|
}
|
|
95
77
|
}
|
|
96
78
|
|
|
97
79
|
private fun showMissedCallNotification(
|
|
98
80
|
context: Context,
|
|
99
81
|
data: Map<String, String>,
|
|
100
|
-
uuid: String
|
|
82
|
+
uuid: String,
|
|
101
83
|
) {
|
|
102
84
|
val name = data["name"] ?: "Unknown"
|
|
103
85
|
val callType = data["callType"] ?: "video"
|
|
@@ -107,40 +89,46 @@ if (!CallState.setCurrent(uuid)) {
|
|
|
107
89
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
108
90
|
|
|
109
91
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
110
|
-
val channel =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
92
|
+
val channel =
|
|
93
|
+
NotificationChannel(
|
|
94
|
+
channelId,
|
|
95
|
+
"Missed Calls",
|
|
96
|
+
NotificationManager.IMPORTANCE_DEFAULT,
|
|
97
|
+
).apply { description = "Missed call notifications" }
|
|
115
98
|
notificationManager.createNotificationChannel(channel)
|
|
116
99
|
}
|
|
117
100
|
|
|
118
101
|
val iconResId =
|
|
119
|
-
context.resources
|
|
102
|
+
context.resources
|
|
103
|
+
.getIdentifier("ic_missed_call", "drawable", context.packageName)
|
|
120
104
|
.takeIf { it != 0 } ?: android.R.drawable.sym_call_missed
|
|
121
105
|
val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
|
|
122
106
|
|
|
123
107
|
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
124
108
|
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
125
|
-
val pendingIntent =
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
109
|
+
val pendingIntent =
|
|
110
|
+
PendingIntent.getActivity(
|
|
111
|
+
context,
|
|
112
|
+
uuid.hashCode(),
|
|
113
|
+
launchIntent,
|
|
114
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
115
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
116
|
+
} else {
|
|
117
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
val builder =
|
|
122
|
+
NotificationCompat
|
|
123
|
+
.Builder(context, channelId)
|
|
124
|
+
.setSmallIcon(iconResId)
|
|
125
|
+
.setContentTitle("$appName • Missed $callType call")
|
|
126
|
+
.setContentText("You missed a call from $name")
|
|
127
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
128
|
+
.setAutoCancel(true)
|
|
129
|
+
.setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
|
130
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
131
|
+
.setContentIntent(pendingIntent)
|
|
144
132
|
|
|
145
133
|
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
146
134
|
}
|
|
@@ -152,7 +140,7 @@ if (!CallState.setCurrent(uuid)) {
|
|
|
152
140
|
val packageName = context.packageName
|
|
153
141
|
return appProcesses.any {
|
|
154
142
|
it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
|
|
155
|
-
|
|
143
|
+
it.processName == packageName
|
|
156
144
|
}
|
|
157
145
|
}
|
|
158
146
|
}
|
|
@@ -19,7 +19,7 @@ object NativeCallManager {
|
|
|
19
19
|
// because the system NotificationManager handles the sound.
|
|
20
20
|
|
|
21
21
|
// Incrementing version to V3 to force fresh channel settings on the device
|
|
22
|
-
const val channelId = "
|
|
22
|
+
const val channelId = "CALL_CHANNEL_V9_URGENT"
|
|
23
23
|
private var currentCallData: Map<String, String>? = null
|
|
24
24
|
|
|
25
25
|
fun getCurrentCallData(): Map<String, String>? = currentCallData
|
|
@@ -139,8 +139,15 @@ object NativeCallManager {
|
|
|
139
139
|
) {
|
|
140
140
|
this.currentCallData = null
|
|
141
141
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
142
|
+
|
|
142
143
|
if (uuid != null) {
|
|
144
|
+
// 1. Kill the notification UI
|
|
143
145
|
notificationManager.cancel(uuid.hashCode())
|
|
146
|
+
|
|
147
|
+
// 2. IMPORTANT: Stop the Foreground Service process
|
|
148
|
+
// This ensures the "Pill" in the status bar goes away
|
|
149
|
+
val serviceIntent = Intent(context, CallForegroundService::class.java)
|
|
150
|
+
context.stopService(serviceIntent)
|
|
144
151
|
}
|
|
145
152
|
}
|
|
146
153
|
|