rns-nativecall 0.1.7 → 0.1.8
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/android/src/main/java/com/rnsnativecall/AcceptCallActivity.kt +28 -12
- package/android/src/main/java/com/rnsnativecall/CallMessagingService.kt +25 -5
- package/android/src/main/java/com/rnsnativecall/CallModule.kt +63 -81
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +34 -28
- package/index.d.ts +2 -6
- package/index.js +2 -4
- package/package.json +1 -1
- package/withNativeCallVoip.js +42 -18
|
@@ -10,29 +10,45 @@ class AcceptCallActivity : Activity() {
|
|
|
10
10
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
11
11
|
super.onCreate(savedInstanceState)
|
|
12
12
|
|
|
13
|
-
// 1. CLEAR THE
|
|
13
|
+
// 1. CLEAR THE NOTIFICATION
|
|
14
|
+
// Use the same ID (101) used in NativeCallManager
|
|
14
15
|
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
15
16
|
notificationManager.cancel(101)
|
|
16
17
|
|
|
17
|
-
// 2.
|
|
18
|
+
// 2. EXTRACT DATA SAFELY
|
|
19
|
+
// We iterate through all extras and convert them to Strings for the JS Map
|
|
18
20
|
val dataMap = mutableMapOf<String, String>()
|
|
19
|
-
intent.extras
|
|
20
|
-
|
|
21
|
+
val extras = intent.extras
|
|
22
|
+
extras?.keySet()?.forEach { key ->
|
|
23
|
+
val value = extras.get(key)
|
|
24
|
+
if (value != null) {
|
|
25
|
+
dataMap[key] = value.toString()
|
|
26
|
+
}
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
// 3.
|
|
29
|
+
// 3. EMIT EVENT TO REACT NATIVE
|
|
30
|
+
// This handles the case where the app is already in the background/foreground
|
|
24
31
|
CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
25
32
|
|
|
26
|
-
// 4.
|
|
33
|
+
// 4. LAUNCH OR BRING MAIN APP TO FOREGROUND
|
|
27
34
|
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
28
|
-
launchIntent
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
if (launchIntent != null) {
|
|
36
|
+
launchIntent.apply {
|
|
37
|
+
// FLAG_ACTIVITY_SINGLE_TOP: Updates the app if it's already open
|
|
38
|
+
// FLAG_ACTIVITY_NEW_TASK: Required when starting from a non-activity context
|
|
39
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
40
|
+
|
|
41
|
+
// Copy all call data to the launch intent
|
|
42
|
+
putExtras(extras ?: Bundle())
|
|
43
|
+
|
|
44
|
+
// Helper flag for your React Native logic
|
|
45
|
+
putExtra("navigatingToCall", true)
|
|
46
|
+
}
|
|
47
|
+
startActivity(launchIntent)
|
|
32
48
|
}
|
|
33
|
-
startActivity(launchIntent)
|
|
34
49
|
|
|
35
|
-
// 5.
|
|
50
|
+
// 5. FINISH TRAMPOLINE
|
|
51
|
+
// This ensures this invisible activity doesn't stay in the "Recent Apps" list
|
|
36
52
|
finish()
|
|
37
53
|
}
|
|
38
54
|
}
|
|
@@ -1,14 +1,34 @@
|
|
|
1
|
-
///
|
|
2
|
-
// Users/bush/Desktop/Packages/rns-nativecall/android/src/main/java/com/rnsnativecall/CallMessagingService.kt
|
|
3
1
|
package com.rnsnativecall
|
|
4
2
|
|
|
3
|
+
import android.app.ActivityManager
|
|
4
|
+
import android.content.Context
|
|
5
5
|
import com.google.firebase.messaging.FirebaseMessagingService
|
|
6
6
|
import com.google.firebase.messaging.RemoteMessage
|
|
7
7
|
|
|
8
8
|
class CallMessagingService : FirebaseMessagingService() {
|
|
9
9
|
|
|
10
10
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
if (isAppInForeground(applicationContext)) {
|
|
12
|
+
// 1. App is OPEN: Don't show the system pill.
|
|
13
|
+
// Just send the data to your React Native listeners.
|
|
14
|
+
CallModule.sendEventToJS("onCallReceived", remoteMessage.data)
|
|
15
|
+
} else {
|
|
16
|
+
// 2. App is CLOSED/BACKGROUND: Show the sticky system pill.
|
|
17
|
+
NativeCallManager.handleIncomingPush(applicationContext, remoteMessage.data)
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
|
-
|
|
20
|
+
|
|
21
|
+
private fun isAppInForeground(context: Context): Boolean {
|
|
22
|
+
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
23
|
+
val appProcesses = activityManager.runningAppProcesses ?: return false
|
|
24
|
+
val packageName = context.packageName
|
|
25
|
+
|
|
26
|
+
for (appProcess in appProcesses) {
|
|
27
|
+
if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
|
|
28
|
+
appProcess.processName == packageName) {
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
|
+
import android.app.NotificationManager
|
|
3
4
|
import android.content.ComponentName
|
|
4
5
|
import android.content.Context
|
|
5
6
|
import android.content.Intent
|
|
@@ -9,11 +10,9 @@ import android.telecom.DisconnectCause
|
|
|
9
10
|
import android.telecom.PhoneAccount
|
|
10
11
|
import android.telecom.PhoneAccountHandle
|
|
11
12
|
import android.telecom.TelecomManager
|
|
13
|
+
import android.widget.Toast
|
|
12
14
|
import com.facebook.react.bridge.*
|
|
13
15
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
14
|
-
import android.widget.Toast
|
|
15
|
-
import com.google.android.material.snackbar.Snackbar
|
|
16
|
-
import android.view.View
|
|
17
16
|
|
|
18
17
|
class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
19
18
|
|
|
@@ -30,45 +29,33 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
private fun registerPhoneAccount() {
|
|
33
|
-
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
34
|
-
val phoneAccountHandle = getPhoneAccountHandle()
|
|
35
|
-
|
|
36
|
-
val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
|
|
37
|
-
|
|
38
|
-
// ✅ FIXED: Removed CAPABILITY_CALL_PROVIDER
|
|
39
|
-
// Self-managed apps only need these specific flags
|
|
40
|
-
val capabilities = PhoneAccount.CAPABILITY_VIDEO_CALLING or
|
|
41
|
-
PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING
|
|
42
|
-
|
|
43
|
-
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, appName)
|
|
44
|
-
.setCapabilities(capabilities)
|
|
45
|
-
.setShortDescription(appName)
|
|
46
|
-
.addSupportedUriScheme("sip")
|
|
47
|
-
.addSupportedUriScheme("tel")
|
|
48
|
-
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
|
|
49
|
-
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
|
|
50
|
-
.build()
|
|
51
|
-
|
|
52
|
-
telecomManager.registerPhoneAccount(phoneAccount)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
@ReactMethod
|
|
56
|
-
fun displayIncomingCall(uuid: String, number: String, name: String, hasVideo: Boolean, playRing: Boolean, promise: Promise) {
|
|
57
32
|
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
58
33
|
val phoneAccountHandle = getPhoneAccountHandle()
|
|
34
|
+
val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
|
|
59
35
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
36
|
+
val capabilities = PhoneAccount.CAPABILITY_VIDEO_CALLING or
|
|
37
|
+
PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING or
|
|
38
|
+
PhoneAccount.CAPABILITY_SELF_MANAGED
|
|
63
39
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
40
|
+
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, appName)
|
|
41
|
+
.setCapabilities(capabilities)
|
|
42
|
+
.setShortDescription(appName)
|
|
43
|
+
.addSupportedUriScheme("sip")
|
|
44
|
+
.addSupportedUriScheme("tel")
|
|
45
|
+
.build()
|
|
68
46
|
|
|
69
|
-
|
|
47
|
+
telecomManager.registerPhoneAccount(phoneAccount)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@ReactMethod
|
|
51
|
+
fun displayIncomingCall(uuid: String, name: String, callType: String, promise: Promise) {
|
|
70
52
|
try {
|
|
71
|
-
|
|
53
|
+
val data = mapOf(
|
|
54
|
+
"callUuid" to uuid,
|
|
55
|
+
"name" to name,
|
|
56
|
+
"callType" to callType
|
|
57
|
+
)
|
|
58
|
+
NativeCallManager.handleIncomingPush(reactApplicationContext, data)
|
|
72
59
|
promise.resolve(true)
|
|
73
60
|
} catch (e: Exception) {
|
|
74
61
|
promise.reject("CALL_ERROR", e.message)
|
|
@@ -77,68 +64,56 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
77
64
|
|
|
78
65
|
@ReactMethod
|
|
79
66
|
fun endNativeCall(uuid: String) {
|
|
67
|
+
val notificationManager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
68
|
+
notificationManager.cancel(101)
|
|
69
|
+
|
|
80
70
|
val connection = MyConnectionService.getConnection(uuid)
|
|
81
71
|
connection?.let {
|
|
82
|
-
it.setDisconnected(DisconnectCause(DisconnectCause.
|
|
72
|
+
it.setDisconnected(DisconnectCause(DisconnectCause.MISSED))
|
|
83
73
|
it.destroy()
|
|
84
74
|
MyConnectionService.removeConnection(uuid)
|
|
85
75
|
}
|
|
86
76
|
}
|
|
87
77
|
|
|
88
|
-
//done // val fallbackIntent = Intent(Intent.ACTION_MAIN).apply {
|
|
89
|
-
// addCategory(Intent.CATEGORY_LAUNCHER)
|
|
90
|
-
// component = ComponentName("com.android.phone", "com.android.phone.CallFeaturesSetting")
|
|
91
|
-
// addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
92
|
-
// }
|
|
93
|
-
// reactApplicationContext.startActivity(fallbackIntent)
|
|
94
|
-
|
|
95
78
|
@ReactMethod
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
val account = telecomManager.getPhoneAccount(phoneAccountHandle)
|
|
102
|
-
if (account != null && account.isEnabled) {
|
|
103
|
-
promise.resolve(true)
|
|
104
|
-
} else {
|
|
105
|
-
try {
|
|
106
|
-
// Show a quick message to guide the user
|
|
107
|
-
val text = "Tap 'Active calling accounts' and then enable $appName"
|
|
108
|
-
val duration = Toast.LENGTH_LONG
|
|
109
|
-
val toast = Toast.makeText(reactApplicationContext, text, duration)
|
|
110
|
-
|
|
111
|
-
toast.show()
|
|
112
|
-
|
|
113
|
-
// Show again after 3.5s
|
|
114
|
-
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
115
|
-
toast.show()
|
|
116
|
-
}, 3500)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// Open Calling Accounts settings
|
|
120
|
-
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
|
|
121
|
-
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
122
|
-
reactApplicationContext.startActivity(intent)
|
|
79
|
+
fun checkTelecomPermissions(promise: Promise) {
|
|
80
|
+
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
81
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
82
|
+
val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
|
|
123
83
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
promise.
|
|
84
|
+
val account = telecomManager.getPhoneAccount(phoneAccountHandle)
|
|
85
|
+
if (account != null && account.isEnabled) {
|
|
86
|
+
promise.resolve(true)
|
|
87
|
+
} else {
|
|
88
|
+
try {
|
|
89
|
+
Toast.makeText(reactApplicationContext, "Tap 'Active calling accounts' and then enable $appName", Toast.LENGTH_LONG).show()
|
|
90
|
+
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
|
|
91
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
92
|
+
reactApplicationContext.startActivity(intent)
|
|
93
|
+
promise.resolve(false)
|
|
94
|
+
} catch (e: Exception) {
|
|
95
|
+
promise.reject("PERMISSION_ERROR", e.message)
|
|
96
|
+
}
|
|
127
97
|
}
|
|
128
98
|
}
|
|
129
|
-
}
|
|
130
99
|
|
|
100
|
+
@ReactMethod
|
|
101
|
+
fun getInitialCallData(promise: Promise) {
|
|
102
|
+
promise.resolve(pendingCallData)
|
|
103
|
+
pendingCallData = null // Clear after consumption
|
|
104
|
+
}
|
|
131
105
|
|
|
132
106
|
@ReactMethod fun addListener(eventName: String) {}
|
|
133
107
|
@ReactMethod fun removeListeners(count: Int) {}
|
|
134
108
|
|
|
135
109
|
companion object {
|
|
136
110
|
private var instance: CallModule? = null
|
|
111
|
+
private var pendingCallData: WritableMap? = null
|
|
137
112
|
|
|
138
113
|
@JvmStatic
|
|
139
114
|
fun sendEventToJS(eventName: String, params: Any?) {
|
|
140
|
-
val reactContext = instance?.reactApplicationContext
|
|
141
|
-
|
|
115
|
+
val reactContext = instance?.reactApplicationContext
|
|
116
|
+
|
|
142
117
|
val bridgeData = when (params) {
|
|
143
118
|
is Map<*, *> -> {
|
|
144
119
|
val map = Arguments.createMap()
|
|
@@ -148,14 +123,21 @@ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
|
148
123
|
map
|
|
149
124
|
}
|
|
150
125
|
is String -> {
|
|
151
|
-
Arguments.createMap().apply { putString("
|
|
126
|
+
Arguments.createMap().apply { putString("callUuid", params) }
|
|
152
127
|
}
|
|
153
128
|
else -> null
|
|
154
129
|
}
|
|
155
130
|
|
|
156
|
-
reactContext
|
|
157
|
-
|
|
158
|
-
|
|
131
|
+
if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
|
|
132
|
+
reactContext
|
|
133
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
134
|
+
?.emit(eventName, bridgeData)
|
|
135
|
+
} else {
|
|
136
|
+
// If app is dead/cold-starting, cache the accept event
|
|
137
|
+
if (eventName == "onCallAccepted") {
|
|
138
|
+
pendingCallData = bridgeData
|
|
139
|
+
}
|
|
140
|
+
}
|
|
159
141
|
}
|
|
160
142
|
}
|
|
161
143
|
}
|
|
@@ -11,49 +11,55 @@ import androidx.core.app.NotificationCompat
|
|
|
11
11
|
object NativeCallManager {
|
|
12
12
|
|
|
13
13
|
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
14
|
-
val uuid = data["
|
|
14
|
+
val uuid = data["callUuid"] ?: return
|
|
15
15
|
val name = data["name"] ?: "Incoming Call"
|
|
16
16
|
val callType = data["callType"] ?: "audio"
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// Use MUTABLE flag to ensure extras are properly passed on Android 12+
|
|
19
|
+
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
20
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
20
21
|
} else {
|
|
21
22
|
PendingIntent.FLAG_UPDATE_CURRENT
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
// 1. DUMMY INTENT:
|
|
25
|
-
// It prevents the notification from disappearing but doesn't open a screen.
|
|
25
|
+
// 1. DUMMY INTENT: Keeps the notification "Sticky" (Persistent)
|
|
26
26
|
val dummyIntent = PendingIntent.getActivity(
|
|
27
27
|
context,
|
|
28
28
|
0,
|
|
29
29
|
Intent(),
|
|
30
|
-
|
|
30
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
// 2. Accept Action
|
|
34
|
-
val acceptIntent = Intent(context, AcceptCallActivity::class.java).apply {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
// 2. Accept Action - Use unique action and requestCode
|
|
34
|
+
val acceptIntent = Intent(context, AcceptCallActivity::class.java).apply {
|
|
35
|
+
// Unique action string prevents intent caching issues
|
|
36
|
+
action = "ACTION_ACCEPT_$uuid"
|
|
37
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
38
|
+
// Flatten the map into the intent extras
|
|
39
|
+
data.forEach { (key, value) -> putExtra(key, value) }
|
|
40
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
val acceptPendingIntent = PendingIntent.getActivity(
|
|
44
|
+
context,
|
|
45
|
+
uuid.hashCode(), // Unique RequestCode
|
|
46
|
+
acceptIntent,
|
|
47
|
+
pendingFlags
|
|
48
|
+
)
|
|
41
49
|
|
|
42
|
-
//
|
|
43
|
-
val acceptPendingIntent = PendingIntent.getActivity(
|
|
44
|
-
context,
|
|
45
|
-
1001,
|
|
46
|
-
acceptIntent,
|
|
47
|
-
pendingFlags
|
|
48
|
-
)
|
|
49
|
-
// 3. Reject Action
|
|
50
|
+
// 3. Reject Action - Use unique action and requestCode
|
|
50
51
|
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
51
|
-
action = "
|
|
52
|
+
action = "ACTION_REJECT_$uuid"
|
|
52
53
|
putExtra("EXTRA_CALL_UUID", uuid)
|
|
53
54
|
}
|
|
54
|
-
val rejectPendingIntent = PendingIntent.getBroadcast(
|
|
55
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(
|
|
56
|
+
context,
|
|
57
|
+
uuid.hashCode() + 1, // Unique RequestCode
|
|
58
|
+
rejectIntent,
|
|
59
|
+
pendingFlags
|
|
60
|
+
)
|
|
55
61
|
|
|
56
|
-
// 4. Setup Channel
|
|
62
|
+
// 4. Setup Channel
|
|
57
63
|
val channelId = "CALL_CHANNEL_ID"
|
|
58
64
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
59
65
|
|
|
@@ -61,7 +67,6 @@ val acceptPendingIntent = PendingIntent.getActivity(
|
|
|
61
67
|
val channel = NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
62
68
|
description = "Shows incoming call notifications"
|
|
63
69
|
enableVibration(true)
|
|
64
|
-
// Critical for bypassing "Do Not Disturb"
|
|
65
70
|
setBypassDnd(true)
|
|
66
71
|
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
67
72
|
}
|
|
@@ -75,10 +80,11 @@ val acceptPendingIntent = PendingIntent.getActivity(
|
|
|
75
80
|
.setContentText(name)
|
|
76
81
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
77
82
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
78
|
-
.setOngoing(true)
|
|
83
|
+
.setOngoing(true)
|
|
79
84
|
.setAutoCancel(false)
|
|
85
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
80
86
|
|
|
81
|
-
// This
|
|
87
|
+
// This hack keeps the Heads-Up "Pill" visible until action is taken
|
|
82
88
|
.setFullScreenIntent(dummyIntent, true)
|
|
83
89
|
|
|
84
90
|
.addAction(0, "Answer", acceptPendingIntent)
|
package/index.d.ts
CHANGED
|
@@ -18,18 +18,14 @@ export interface CallHandlerType {
|
|
|
18
18
|
/**
|
|
19
19
|
* Display an incoming call UI using ConnectionService (Android) or CallKit (iOS).
|
|
20
20
|
* @param uuid Unique call identifier
|
|
21
|
-
* @param number Caller number (or URI)
|
|
22
21
|
* @param name Caller display name
|
|
23
|
-
* @param
|
|
24
|
-
* @param shouldRing True to play native ringtone (default: true)
|
|
22
|
+
* @param callType True if video call (default: false)
|
|
25
23
|
* @returns Promise resolving to true if successfully displayed
|
|
26
24
|
*/
|
|
27
25
|
displayCall(
|
|
28
26
|
uuid: string,
|
|
29
|
-
number: string,
|
|
30
27
|
name: string,
|
|
31
|
-
|
|
32
|
-
shouldRing?: boolean
|
|
28
|
+
callType: string,
|
|
33
29
|
): Promise<boolean>;
|
|
34
30
|
|
|
35
31
|
/**
|
package/index.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function ensureAndroidPermissions() {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export const CallHandler = {
|
|
38
|
-
displayCall: async (uuid,
|
|
38
|
+
displayCall: async (uuid, name, callType) => {
|
|
39
39
|
if (!CallModule) return false;
|
|
40
40
|
|
|
41
41
|
// if (Platform.OS === 'android') {
|
|
@@ -46,10 +46,8 @@ export const CallHandler = {
|
|
|
46
46
|
try {
|
|
47
47
|
return await CallModule.displayIncomingCall(
|
|
48
48
|
uuid.toLowerCase().trim(),
|
|
49
|
-
number,
|
|
50
49
|
name,
|
|
51
|
-
|
|
52
|
-
shouldRing
|
|
50
|
+
callType,
|
|
53
51
|
);
|
|
54
52
|
} catch (e) {
|
|
55
53
|
// console.log("Native Call Error:", e);
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
|
-
const { withAndroidManifest, withInfoPlist, withPlugins } = require('@expo/config-plugins');
|
|
1
|
+
const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity } = require('@expo/config-plugins');
|
|
2
2
|
|
|
3
|
+
/** 1. ANDROID MAIN ACTIVITY MOD (Fixes data passing) **/
|
|
4
|
+
function withMainActivityDataFix(config) {
|
|
5
|
+
return withMainActivity(config, (config) => {
|
|
6
|
+
let contents = config.modResults.contents;
|
|
7
|
+
|
|
8
|
+
// Add necessary import
|
|
9
|
+
if (!contents.includes('import android.content.Intent')) {
|
|
10
|
+
contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Add onNewIntent override to ensure data is updated when app is already open
|
|
14
|
+
const onNewIntentCode = `
|
|
15
|
+
override fun onNewIntent(intent: Intent) {
|
|
16
|
+
super.onNewIntent(intent)
|
|
17
|
+
setIntent(intent)
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
if (!contents.includes('override fun onNewIntent')) {
|
|
22
|
+
// Find the last closing brace and insert before it
|
|
23
|
+
const lastBraceIndex = contents.lastIndexOf('}');
|
|
24
|
+
contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
config.modResults.contents = contents;
|
|
28
|
+
return config;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 2. ANDROID MANIFEST CONFIG **/
|
|
3
33
|
function withAndroidConfig(config) {
|
|
4
34
|
return withAndroidManifest(config, (config) => {
|
|
5
35
|
const manifest = config.modResults;
|
|
6
36
|
const application = manifest.manifest.application[0];
|
|
7
37
|
|
|
8
|
-
//
|
|
38
|
+
// Permissions
|
|
9
39
|
const permissions = [
|
|
10
40
|
'android.permission.READ_PHONE_NUMBERS',
|
|
11
41
|
'android.permission.CALL_PHONE',
|
|
@@ -23,22 +53,16 @@ function withAndroidConfig(config) {
|
|
|
23
53
|
}
|
|
24
54
|
});
|
|
25
55
|
|
|
26
|
-
//
|
|
56
|
+
// Activities
|
|
27
57
|
application.activity = application.activity || [];
|
|
28
58
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'android:name': 'com.rnsnativecall.IncomingCallActivity',
|
|
34
|
-
'android:showOnLockScreen': 'true',
|
|
35
|
-
'android:launchMode': 'singleInstance',
|
|
36
|
-
'android:theme': '@style/Theme.AppCompat.Light.NoActionBar'
|
|
37
|
-
}
|
|
38
|
-
});
|
|
59
|
+
// Update MainActivity to use singleTask for better call handling
|
|
60
|
+
const mainActivity = application.activity.find(a => a.$['android:name'] === '.MainActivity');
|
|
61
|
+
if (mainActivity) {
|
|
62
|
+
mainActivity.$['android:launchMode'] = 'singleTask';
|
|
39
63
|
}
|
|
40
64
|
|
|
41
|
-
// AcceptCallActivity (
|
|
65
|
+
// AcceptCallActivity (Trampoline)
|
|
42
66
|
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
|
|
43
67
|
application.activity.push({
|
|
44
68
|
$: {
|
|
@@ -52,7 +76,7 @@ function withAndroidConfig(config) {
|
|
|
52
76
|
});
|
|
53
77
|
}
|
|
54
78
|
|
|
55
|
-
//
|
|
79
|
+
// Services
|
|
56
80
|
application.service = application.service || [];
|
|
57
81
|
const services = [
|
|
58
82
|
{ name: 'com.rnsnativecall.MyConnectionService', permission: 'android.permission.BIND_CONNECTION_SERVICE', action: 'android.telecom.ConnectionService' },
|
|
@@ -68,7 +92,7 @@ function withAndroidConfig(config) {
|
|
|
68
92
|
}
|
|
69
93
|
});
|
|
70
94
|
|
|
71
|
-
//
|
|
95
|
+
// Receivers
|
|
72
96
|
application.receiver = application.receiver || [];
|
|
73
97
|
if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
|
|
74
98
|
application.receiver.push({
|
|
@@ -80,7 +104,7 @@ function withAndroidConfig(config) {
|
|
|
80
104
|
});
|
|
81
105
|
}
|
|
82
106
|
|
|
83
|
-
/** IOS
|
|
107
|
+
/** 3. IOS CONFIG **/
|
|
84
108
|
function withIosConfig(config) {
|
|
85
109
|
return withInfoPlist(config, (config) => {
|
|
86
110
|
const infoPlist = config.modResults;
|
|
@@ -91,5 +115,5 @@ function withIosConfig(config) {
|
|
|
91
115
|
}
|
|
92
116
|
|
|
93
117
|
module.exports = (config) => {
|
|
94
|
-
return withPlugins(config, [withAndroidConfig, withIosConfig]);
|
|
118
|
+
return withPlugins(config, [withAndroidConfig, withMainActivityDataFix, withIosConfig]);
|
|
95
119
|
};
|