rns-nativecall 0.1.7 → 0.1.9
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 +29 -13
- package/android/src/main/java/com/rnsnativecall/CallActionReceiver.kt +3 -0
- package/android/src/main/java/com/rnsnativecall/CallMessagingService.kt +25 -5
- package/android/src/main/java/com/rnsnativecall/CallModule.kt +64 -81
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +51 -28
- package/index.d.ts +12 -20
- package/index.js +32 -25
- package/ios/CallModule.h +4 -0
- package/ios/CallModule.m +33 -44
- package/package.json +2 -1
- package/withNativeCallVoip.js +42 -19
- package/android/src/main/java/com/rnsnativecall/IncomingCallActivity.kt +0 -74
- package/android/src/main/res/drawable/circle_green.xml +0 -4
- package/android/src/main/res/drawable/circle_red.xml +0 -4
- package/android/src/main/res/drawable/pill_background.xml +0 -5
- package/android/src/main/res/layout/activity_incoming_call.xml +0 -67
- package/android/src/main/res/values/styles.xml +0 -12
|
@@ -9,30 +9,46 @@ import android.os.Bundle
|
|
|
9
9
|
class AcceptCallActivity : Activity() {
|
|
10
10
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
11
11
|
super.onCreate(savedInstanceState)
|
|
12
|
-
|
|
13
|
-
// 1. CLEAR THE
|
|
12
|
+
NativeCallManager.stopRingtone()
|
|
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
|
}
|
|
@@ -8,6 +8,7 @@ import android.os.Bundle
|
|
|
8
8
|
|
|
9
9
|
class CallActionReceiver : BroadcastReceiver() {
|
|
10
10
|
override fun onReceive(context: Context, intent: Intent) {
|
|
11
|
+
NativeCallManager.stopRingtone()
|
|
11
12
|
val uuid = intent.getStringExtra("EXTRA_CALL_UUID")
|
|
12
13
|
|
|
13
14
|
// 1. Clear the notification
|
|
@@ -15,6 +16,7 @@ class CallActionReceiver : BroadcastReceiver() {
|
|
|
15
16
|
notificationManager.cancel(101)
|
|
16
17
|
|
|
17
18
|
if (intent.action == "ACTION_ACCEPT") {
|
|
19
|
+
NativeCallManager.stopRingtone()
|
|
18
20
|
// 2. Prepare the data for React Native
|
|
19
21
|
val dataMap = mutableMapOf<String, String>()
|
|
20
22
|
intent.extras?.keySet()?.forEach { key ->
|
|
@@ -34,6 +36,7 @@ class CallActionReceiver : BroadcastReceiver() {
|
|
|
34
36
|
}
|
|
35
37
|
context.startActivity(launchIntent)
|
|
36
38
|
} else {
|
|
39
|
+
NativeCallManager.stopRingtone()
|
|
37
40
|
// Logic for Reject
|
|
38
41
|
CallModule.sendEventToJS("onCallRejected", mapOf("callUUID" to uuid))
|
|
39
42
|
}
|
|
@@ -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,57 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
77
64
|
|
|
78
65
|
@ReactMethod
|
|
79
66
|
fun endNativeCall(uuid: String) {
|
|
67
|
+
NativeCallManager.stopRingtone()
|
|
68
|
+
val notificationManager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
69
|
+
notificationManager.cancel(101)
|
|
70
|
+
|
|
80
71
|
val connection = MyConnectionService.getConnection(uuid)
|
|
81
72
|
connection?.let {
|
|
82
|
-
it.setDisconnected(DisconnectCause(DisconnectCause.
|
|
73
|
+
it.setDisconnected(DisconnectCause(DisconnectCause.MISSED))
|
|
83
74
|
it.destroy()
|
|
84
75
|
MyConnectionService.removeConnection(uuid)
|
|
85
76
|
}
|
|
86
77
|
}
|
|
87
78
|
|
|
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
79
|
@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)
|
|
80
|
+
fun checkTelecomPermissions(promise: Promise) {
|
|
81
|
+
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
82
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
83
|
+
val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
|
|
123
84
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
promise.
|
|
85
|
+
val account = telecomManager.getPhoneAccount(phoneAccountHandle)
|
|
86
|
+
if (account != null && account.isEnabled) {
|
|
87
|
+
promise.resolve(true)
|
|
88
|
+
} else {
|
|
89
|
+
try {
|
|
90
|
+
Toast.makeText(reactApplicationContext, "Tap 'Active calling accounts' and then enable $appName", Toast.LENGTH_LONG).show()
|
|
91
|
+
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
|
|
92
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
93
|
+
reactApplicationContext.startActivity(intent)
|
|
94
|
+
promise.resolve(false)
|
|
95
|
+
} catch (e: Exception) {
|
|
96
|
+
promise.reject("PERMISSION_ERROR", e.message)
|
|
97
|
+
}
|
|
127
98
|
}
|
|
128
99
|
}
|
|
129
|
-
}
|
|
130
100
|
|
|
101
|
+
@ReactMethod
|
|
102
|
+
fun getInitialCallData(promise: Promise) {
|
|
103
|
+
promise.resolve(pendingCallData)
|
|
104
|
+
pendingCallData = null // Clear after consumption
|
|
105
|
+
}
|
|
131
106
|
|
|
132
107
|
@ReactMethod fun addListener(eventName: String) {}
|
|
133
108
|
@ReactMethod fun removeListeners(count: Int) {}
|
|
134
109
|
|
|
135
110
|
companion object {
|
|
136
111
|
private var instance: CallModule? = null
|
|
112
|
+
private var pendingCallData: WritableMap? = null
|
|
137
113
|
|
|
138
114
|
@JvmStatic
|
|
139
115
|
fun sendEventToJS(eventName: String, params: Any?) {
|
|
140
|
-
val reactContext = instance?.reactApplicationContext
|
|
141
|
-
|
|
116
|
+
val reactContext = instance?.reactApplicationContext
|
|
117
|
+
|
|
142
118
|
val bridgeData = when (params) {
|
|
143
119
|
is Map<*, *> -> {
|
|
144
120
|
val map = Arguments.createMap()
|
|
@@ -148,14 +124,21 @@ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
|
148
124
|
map
|
|
149
125
|
}
|
|
150
126
|
is String -> {
|
|
151
|
-
Arguments.createMap().apply { putString("
|
|
127
|
+
Arguments.createMap().apply { putString("callUuid", params) }
|
|
152
128
|
}
|
|
153
129
|
else -> null
|
|
154
130
|
}
|
|
155
131
|
|
|
156
|
-
reactContext
|
|
157
|
-
|
|
158
|
-
|
|
132
|
+
if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
|
|
133
|
+
reactContext
|
|
134
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
135
|
+
?.emit(eventName, bridgeData)
|
|
136
|
+
} else {
|
|
137
|
+
// If app is dead/cold-starting, cache the accept event
|
|
138
|
+
if (eventName == "onCallAccepted") {
|
|
139
|
+
pendingCallData = bridgeData
|
|
140
|
+
}
|
|
141
|
+
}
|
|
159
142
|
}
|
|
160
143
|
}
|
|
161
144
|
}
|
|
@@ -7,53 +7,64 @@ import android.content.Context
|
|
|
7
7
|
import android.content.Intent
|
|
8
8
|
import android.os.Build
|
|
9
9
|
import androidx.core.app.NotificationCompat
|
|
10
|
+
import android.media.Ringtone
|
|
11
|
+
import android.media.RingtoneManager
|
|
10
12
|
|
|
11
13
|
object NativeCallManager {
|
|
12
14
|
|
|
15
|
+
private var ringtone: Ringtone? = null
|
|
16
|
+
|
|
13
17
|
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
14
|
-
val uuid = data["
|
|
18
|
+
val uuid = data["callUuid"] ?: return
|
|
19
|
+
stopRingtone()
|
|
15
20
|
val name = data["name"] ?: "Incoming Call"
|
|
16
21
|
val callType = data["callType"] ?: "audio"
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
// Use MUTABLE flag to ensure extras are properly passed on Android 12+
|
|
24
|
+
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
25
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
20
26
|
} else {
|
|
21
27
|
PendingIntent.FLAG_UPDATE_CURRENT
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
// 1. DUMMY INTENT:
|
|
25
|
-
// It prevents the notification from disappearing but doesn't open a screen.
|
|
30
|
+
// 1. DUMMY INTENT: Keeps the notification "Sticky" (Persistent)
|
|
26
31
|
val dummyIntent = PendingIntent.getActivity(
|
|
27
32
|
context,
|
|
28
33
|
0,
|
|
29
34
|
Intent(),
|
|
30
|
-
|
|
35
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
|
31
36
|
)
|
|
32
37
|
|
|
33
|
-
// 2. Accept Action
|
|
34
|
-
val acceptIntent = Intent(context, AcceptCallActivity::class.java).apply {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// 2. Accept Action - Use unique action and requestCode
|
|
39
|
+
val acceptIntent = Intent(context, AcceptCallActivity::class.java).apply {
|
|
40
|
+
// Unique action string prevents intent caching issues
|
|
41
|
+
action = "ACTION_ACCEPT_$uuid"
|
|
42
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
43
|
+
// Flatten the map into the intent extras
|
|
44
|
+
data.forEach { (key, value) -> putExtra(key, value) }
|
|
45
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
val acceptPendingIntent = PendingIntent.getActivity(
|
|
49
|
+
context,
|
|
50
|
+
uuid.hashCode(), // Unique RequestCode
|
|
51
|
+
acceptIntent,
|
|
52
|
+
pendingFlags
|
|
53
|
+
)
|
|
41
54
|
|
|
42
|
-
//
|
|
43
|
-
val acceptPendingIntent = PendingIntent.getActivity(
|
|
44
|
-
context,
|
|
45
|
-
1001,
|
|
46
|
-
acceptIntent,
|
|
47
|
-
pendingFlags
|
|
48
|
-
)
|
|
49
|
-
// 3. Reject Action
|
|
55
|
+
// 3. Reject Action - Use unique action and requestCode
|
|
50
56
|
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
51
|
-
action = "
|
|
57
|
+
action = "ACTION_REJECT_$uuid"
|
|
52
58
|
putExtra("EXTRA_CALL_UUID", uuid)
|
|
53
59
|
}
|
|
54
|
-
val rejectPendingIntent = PendingIntent.getBroadcast(
|
|
60
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(
|
|
61
|
+
context,
|
|
62
|
+
uuid.hashCode() + 1, // Unique RequestCode
|
|
63
|
+
rejectIntent,
|
|
64
|
+
pendingFlags
|
|
65
|
+
)
|
|
55
66
|
|
|
56
|
-
// 4. Setup Channel
|
|
67
|
+
// 4. Setup Channel
|
|
57
68
|
val channelId = "CALL_CHANNEL_ID"
|
|
58
69
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
59
70
|
|
|
@@ -61,7 +72,6 @@ val acceptPendingIntent = PendingIntent.getActivity(
|
|
|
61
72
|
val channel = NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
62
73
|
description = "Shows incoming call notifications"
|
|
63
74
|
enableVibration(true)
|
|
64
|
-
// Critical for bypassing "Do Not Disturb"
|
|
65
75
|
setBypassDnd(true)
|
|
66
76
|
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
67
77
|
}
|
|
@@ -75,15 +85,28 @@ val acceptPendingIntent = PendingIntent.getActivity(
|
|
|
75
85
|
.setContentText(name)
|
|
76
86
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
77
87
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
78
|
-
.setOngoing(true)
|
|
88
|
+
.setOngoing(true)
|
|
79
89
|
.setAutoCancel(false)
|
|
90
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
80
91
|
|
|
81
|
-
// This
|
|
92
|
+
// This hack keeps the Heads-Up "Pill" visible until action is taken
|
|
82
93
|
.setFullScreenIntent(dummyIntent, true)
|
|
83
94
|
|
|
84
95
|
.addAction(0, "Answer", acceptPendingIntent)
|
|
85
96
|
.addAction(0, "Decline", rejectPendingIntent)
|
|
86
97
|
|
|
87
98
|
notificationManager.notify(101, builder.build())
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
102
|
+
ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
|
|
103
|
+
ringtone?.play()
|
|
104
|
+
} catch (e: Exception) {
|
|
105
|
+
e.printStackTrace()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
fun stopRingtone() {
|
|
109
|
+
ringtone?.stop()
|
|
110
|
+
ringtone = null
|
|
88
111
|
}
|
|
89
112
|
}
|
package/index.d.ts
CHANGED
|
@@ -1,49 +1,41 @@
|
|
|
1
|
-
///Users/bush/Desktop/Apps/Raiidr/package/index.d.ts
|
|
2
|
-
|
|
3
1
|
export interface CallData {
|
|
4
|
-
|
|
2
|
+
callUuid: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
callType?: 'audio' | 'video';
|
|
5
|
+
[key: string]: any; // To allow for custom FCM payload data
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
export type CallAcceptedCallback = (data: CallData) => void;
|
|
8
9
|
export type CallRejectedCallback = (data: CallData) => void;
|
|
9
|
-
export type CallFailedCallback = (data:
|
|
10
|
+
export type CallFailedCallback = (data: any) => void;
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Manually request/check Android permissions for Telecom.
|
|
13
|
-
* @returns Promise resolving to true if granted
|
|
14
14
|
*/
|
|
15
15
|
export function ensureAndroidPermissions(): Promise<boolean>;
|
|
16
16
|
|
|
17
17
|
export interface CallHandlerType {
|
|
18
18
|
/**
|
|
19
|
-
* Display
|
|
19
|
+
* Display the Sticky Pill notification UI.
|
|
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)
|
|
25
|
-
* @returns Promise resolving to true if successfully displayed
|
|
22
|
+
* @param callType 'audio' or 'video'
|
|
26
23
|
*/
|
|
27
24
|
displayCall(
|
|
28
25
|
uuid: string,
|
|
29
|
-
number: string,
|
|
30
26
|
name: string,
|
|
31
|
-
|
|
32
|
-
shouldRing?: boolean
|
|
27
|
+
callType: 'audio' | 'video',
|
|
33
28
|
): Promise<boolean>;
|
|
34
29
|
|
|
35
30
|
/**
|
|
36
|
-
* Dismiss the native call UI (
|
|
37
|
-
* @param uuid Call identifier
|
|
31
|
+
* Dismiss the native call UI (Sticky Pill).
|
|
38
32
|
*/
|
|
39
33
|
destroyNativeCallUI(uuid: string): void;
|
|
40
34
|
|
|
41
35
|
/**
|
|
42
|
-
* Subscribe to call events
|
|
43
|
-
*
|
|
44
|
-
* @
|
|
45
|
-
* @param onFailed Optional callback for system-level failures
|
|
46
|
-
* @returns Function to unsubscribe all listeners
|
|
36
|
+
* Subscribe to call events. Automatically checks for cold-start data
|
|
37
|
+
* if the app was opened via the Answer button.
|
|
38
|
+
* @returns Function to unsubscribe
|
|
47
39
|
*/
|
|
48
40
|
subscribe(
|
|
49
41
|
onAccept: CallAcceptedCallback,
|
package/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
///Users/bush/Desktop/Apps/Raiidr/package/index.js
|
|
2
1
|
import {
|
|
3
2
|
NativeModules,
|
|
4
3
|
NativeEventEmitter,
|
|
@@ -8,11 +7,10 @@ import {
|
|
|
8
7
|
|
|
9
8
|
const { CallModule } = NativeModules;
|
|
10
9
|
|
|
11
|
-
// Safety check for the Native Module
|
|
12
10
|
if (!CallModule && __DEV__) {
|
|
13
11
|
console.warn(
|
|
14
12
|
"rns-nativecall: NativeModule.CallModule is undefined. " +
|
|
15
|
-
"Make sure you have rebuilt your native project
|
|
13
|
+
"Make sure you have rebuilt your native project."
|
|
16
14
|
);
|
|
17
15
|
}
|
|
18
16
|
|
|
@@ -25,59 +23,68 @@ const REQUIRED_PERMISSIONS = Platform.OS === 'android' ? [
|
|
|
25
23
|
|
|
26
24
|
export async function ensureAndroidPermissions() {
|
|
27
25
|
if (Platform.OS !== 'android') return true;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
try {
|
|
27
|
+
const result = await PermissionsAndroid.requestMultiple(REQUIRED_PERMISSIONS);
|
|
28
|
+
const isAccountEnabled = await CallModule.checkTelecomPermissions();
|
|
29
|
+
if (!isAccountEnabled) return false;
|
|
30
|
+
return Object.values(result).every(status => status === PermissionsAndroid.RESULTS.GRANTED);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
export const CallHandler = {
|
|
38
|
-
displayCall: async (uuid,
|
|
37
|
+
displayCall: async (uuid, name, callType = "audio") => {
|
|
39
38
|
if (!CallModule) return false;
|
|
40
|
-
|
|
41
|
-
// if (Platform.OS === 'android') {
|
|
42
|
-
// const hasPerms = await ensureAndroidPermissions();
|
|
43
|
-
// if (!hasPerms) return false;
|
|
44
|
-
// }
|
|
45
|
-
|
|
46
39
|
try {
|
|
47
40
|
return await CallModule.displayIncomingCall(
|
|
48
41
|
uuid.toLowerCase().trim(),
|
|
49
|
-
number,
|
|
50
42
|
name,
|
|
51
|
-
|
|
52
|
-
shouldRing
|
|
43
|
+
callType
|
|
53
44
|
);
|
|
54
45
|
} catch (e) {
|
|
55
|
-
// console.log("Native Call Error:", e);
|
|
56
46
|
return false;
|
|
57
47
|
}
|
|
58
48
|
},
|
|
59
49
|
|
|
60
50
|
destroyNativeCallUI: (uuid) => {
|
|
61
51
|
if (CallModule?.endNativeCall) {
|
|
62
|
-
CallModule.endNativeCall(uuid.toLowerCase());
|
|
52
|
+
CallModule.endNativeCall(uuid.toLowerCase().trim());
|
|
63
53
|
}
|
|
64
54
|
},
|
|
65
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Subscribes to call events and checks for initial cold-start data.
|
|
58
|
+
*/
|
|
66
59
|
subscribe: (onAccept, onReject, onFailed) => {
|
|
67
60
|
if (!callEventEmitter) return () => { };
|
|
68
61
|
|
|
69
62
|
const subs = [
|
|
70
|
-
callEventEmitter.addListener('onCallAccepted',
|
|
71
|
-
|
|
63
|
+
callEventEmitter.addListener('onCallAccepted', (data) => {
|
|
64
|
+
onAccept(data);
|
|
65
|
+
}),
|
|
66
|
+
callEventEmitter.addListener('onCallRejected', (data) => {
|
|
67
|
+
onReject(data);
|
|
68
|
+
}),
|
|
72
69
|
];
|
|
73
70
|
|
|
74
71
|
if (onFailed) {
|
|
75
72
|
subs.push(callEventEmitter.addListener('onCallFailed', onFailed));
|
|
76
73
|
}
|
|
77
74
|
|
|
75
|
+
// --- COLD START LOGIC ---
|
|
76
|
+
// If the app was killed and opened via the Answer button,
|
|
77
|
+
// the event might have fired before this listener was ready.
|
|
78
|
+
if (Platform.OS === 'android' && CallModule.getInitialCallData) {
|
|
79
|
+
CallModule.getInitialCallData().then((data) => {
|
|
80
|
+
if (data) {
|
|
81
|
+
onAccept(data);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
return () => subs.forEach(s => s.remove());
|
|
79
87
|
}
|
|
80
88
|
};
|
|
81
89
|
|
|
82
|
-
// IMPORTANT: Exporting as default ensures "import CallHandler from '...'" works
|
|
83
90
|
export default CallHandler;
|
package/ios/CallModule.h
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#import <React/RCTBridgeModule.h>
|
|
2
2
|
#import <React/RCTEventEmitter.h>
|
|
3
3
|
#import <CallKit/CallKit.h>
|
|
4
|
+
#import <AVFoundation/AVFoundation.h>
|
|
5
|
+
|
|
4
6
|
@interface CallModule : RCTEventEmitter <RCTBridgeModule, CXProviderDelegate>
|
|
5
7
|
@property(nonatomic, strong) CXProvider *provider;
|
|
8
|
+
// Add this property
|
|
9
|
+
@property(nonatomic, strong) NSUUID *currentCallUUID;
|
|
6
10
|
@end
|
package/ios/CallModule.m
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
RCT_EXPORT_MODULE();
|
|
6
6
|
|
|
7
|
+
// Crucial: This keeps the module alive and on the main thread
|
|
8
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
9
|
+
return YES;
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
- (NSArray<NSString *> *)supportedEvents {
|
|
8
13
|
return @[ @"onCallAccepted", @"onCallRejected", @"onCallFailed" ];
|
|
9
14
|
}
|
|
@@ -11,22 +16,15 @@ RCT_EXPORT_MODULE();
|
|
|
11
16
|
- (instancetype)init {
|
|
12
17
|
self = [super init];
|
|
13
18
|
if (self) {
|
|
14
|
-
|
|
15
|
-
NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
|
|
16
|
-
if (!appName) {
|
|
17
|
-
appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
|
|
18
|
-
}
|
|
19
|
+
NSString *appName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"] ?: [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"];
|
|
19
20
|
|
|
20
|
-
#pragma clang diagnostic push
|
|
21
|
-
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
22
21
|
CXProviderConfiguration *config = [[CXProviderConfiguration alloc] initWithLocalizedName:appName];
|
|
23
|
-
#pragma clang diagnostic pop
|
|
24
|
-
|
|
25
22
|
config.supportsVideo = YES;
|
|
26
23
|
config.maximumCallGroups = 1;
|
|
27
24
|
config.maximumCallsPerCallGroup = 1;
|
|
28
25
|
config.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypeGeneric)];
|
|
29
|
-
|
|
26
|
+
// Add this to prevent system confusion
|
|
27
|
+
config.ringtoneSound = @"Ringtone.caf";
|
|
30
28
|
|
|
31
29
|
self.provider = [[CXProvider alloc] initWithConfiguration:config];
|
|
32
30
|
[self.provider setDelegate:self queue:nil];
|
|
@@ -34,32 +32,33 @@ RCT_EXPORT_MODULE();
|
|
|
34
32
|
return self;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
// Matches the 5 arguments in your JS/Android implementation
|
|
38
35
|
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
|
|
39
|
-
number:(NSString *)number
|
|
40
36
|
name:(NSString *)name
|
|
41
|
-
|
|
42
|
-
showRing:(BOOL)showRing
|
|
37
|
+
callType:(NSString *)callType
|
|
43
38
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
44
39
|
reject:(RCTPromiseRejectBlock)reject)
|
|
45
40
|
{
|
|
46
41
|
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
|
47
42
|
if (!uuid) {
|
|
48
|
-
reject(@"INVALID_UUID", @"
|
|
43
|
+
reject(@"INVALID_UUID", @"Invalid UUID", nil);
|
|
49
44
|
return;
|
|
50
45
|
}
|
|
46
|
+
|
|
47
|
+
self.currentCallUUID = uuid;
|
|
48
|
+
|
|
49
|
+
// 1. CONFIGURE AUDIO SESSION FIRST (Prevents Auto-Drop)
|
|
50
|
+
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
51
|
+
NSError *error = nil;
|
|
52
|
+
[session setCategory:AVAudioSessionCategoryPlayAndRecord
|
|
53
|
+
mode:AVAudioSessionModeVoiceChat
|
|
54
|
+
options:AVAudioSessionCategoryOptionAllowBluetooth
|
|
55
|
+
error:&error];
|
|
51
56
|
|
|
52
57
|
CXCallUpdate *update = [[CXCallUpdate alloc] init];
|
|
53
|
-
// We use 'name' here so the system UI shows the caller's name instead of just their number
|
|
54
58
|
update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:name];
|
|
55
|
-
update.hasVideo =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
update.supportsHolding = NO;
|
|
59
|
-
|
|
60
|
-
[self.provider reportNewIncomingCallWithUUID:uuid
|
|
61
|
-
update:update
|
|
62
|
-
completion:^(NSError *_Nullable error) {
|
|
59
|
+
update.hasVideo = [callType isEqualToString:@"video"];
|
|
60
|
+
|
|
61
|
+
[self.provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) {
|
|
63
62
|
if (error) {
|
|
64
63
|
reject(@"CALL_ERROR", error.localizedDescription, error);
|
|
65
64
|
} else {
|
|
@@ -68,35 +67,25 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
|
|
|
68
67
|
}];
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString) {
|
|
72
|
-
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
|
|
73
|
-
if (uuid) {
|
|
74
|
-
[self.provider reportCallWithUUID:uuid
|
|
75
|
-
endedAtDate:[NSDate date]
|
|
76
|
-
reason:CXCallEndedReasonRemoteEnded];
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Placeholder for Android parity
|
|
81
|
-
RCT_EXPORT_METHOD(checkTelecomPermissions:(RCTPromiseResolveBlock)resolve
|
|
82
|
-
reject:(RCTPromiseRejectBlock)reject) {
|
|
83
|
-
resolve(@YES);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
70
|
// MARK: - CXProviderDelegate
|
|
87
71
|
|
|
88
72
|
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
|
|
73
|
+
// 2. ACTIVATE AUDIO SESSION (Crucial for CallKit)
|
|
74
|
+
[[AVAudioSession sharedInstance] setActive:YES error:nil];
|
|
75
|
+
|
|
89
76
|
[action fulfill];
|
|
90
|
-
[self sendEventWithName:@"onCallAccepted"
|
|
91
|
-
body:@{@"callUUID" : [action.callUUID.UUIDString lowercaseString]}];
|
|
77
|
+
[self sendEventWithName:@"onCallAccepted" body:@{@"callUuid": [action.callUUID.UUIDString lowercaseString]}];
|
|
92
78
|
}
|
|
93
79
|
|
|
94
80
|
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
|
|
95
81
|
[action fulfill];
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
self.currentCallUUID = nil;
|
|
83
|
+
[self sendEventWithName:@"onCallRejected" body:@{@"callUuid": [action.callUUID.UUIDString lowercaseString]}];
|
|
98
84
|
}
|
|
99
85
|
|
|
100
|
-
- (void)providerDidReset:(CXProvider *)provider {
|
|
86
|
+
- (void)providerDidReset:(CXProvider *)provider {
|
|
87
|
+
// Stop all audio if provider resets
|
|
88
|
+
[[AVAudioSession sharedInstance] setActive:NO error:nil];
|
|
89
|
+
}
|
|
101
90
|
|
|
102
91
|
@end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rns-nativecall",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "RNS nativecall component with native Android/iOS for handling native call ui, when app is not open or open.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"p": "npm publish --access public"
|
|
20
20
|
},
|
|
21
21
|
"expo": {
|
|
22
|
+
"autolink": true,
|
|
22
23
|
"plugins": [
|
|
23
24
|
"./app.plugin.js"
|
|
24
25
|
]
|
package/withNativeCallVoip.js
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
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 **/
|
|
4
|
+
function withMainActivityDataFix(config) {
|
|
5
|
+
return withMainActivity(config, (config) => {
|
|
6
|
+
let contents = config.modResults.contents;
|
|
7
|
+
|
|
8
|
+
// Ensure Intent import exists
|
|
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 to catch data when app is 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
|
+
const lastBraceIndex = contents.lastIndexOf('}');
|
|
23
|
+
contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
config.modResults.contents = contents;
|
|
27
|
+
return config;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 2. ANDROID MANIFEST CONFIG **/
|
|
3
32
|
function withAndroidConfig(config) {
|
|
4
33
|
return withAndroidManifest(config, (config) => {
|
|
5
34
|
const manifest = config.modResults;
|
|
6
35
|
const application = manifest.manifest.application[0];
|
|
7
36
|
|
|
8
|
-
//
|
|
37
|
+
// Permissions
|
|
9
38
|
const permissions = [
|
|
10
39
|
'android.permission.READ_PHONE_NUMBERS',
|
|
11
40
|
'android.permission.CALL_PHONE',
|
|
@@ -23,22 +52,16 @@ function withAndroidConfig(config) {
|
|
|
23
52
|
}
|
|
24
53
|
});
|
|
25
54
|
|
|
26
|
-
//
|
|
55
|
+
// Activities
|
|
27
56
|
application.activity = application.activity || [];
|
|
28
57
|
|
|
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
|
-
});
|
|
58
|
+
// Ensure MainActivity is singleTask
|
|
59
|
+
const mainActivity = application.activity.find(a => a.$['android:name'] === '.MainActivity');
|
|
60
|
+
if (mainActivity) {
|
|
61
|
+
mainActivity.$['android:launchMode'] = 'singleTask';
|
|
39
62
|
}
|
|
40
63
|
|
|
41
|
-
// AcceptCallActivity (The
|
|
64
|
+
// AcceptCallActivity (The Trampoline - Must remain in Manifest)
|
|
42
65
|
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
|
|
43
66
|
application.activity.push({
|
|
44
67
|
$: {
|
|
@@ -52,7 +75,7 @@ function withAndroidConfig(config) {
|
|
|
52
75
|
});
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
//
|
|
78
|
+
// Services
|
|
56
79
|
application.service = application.service || [];
|
|
57
80
|
const services = [
|
|
58
81
|
{ name: 'com.rnsnativecall.MyConnectionService', permission: 'android.permission.BIND_CONNECTION_SERVICE', action: 'android.telecom.ConnectionService' },
|
|
@@ -62,13 +85,13 @@ function withAndroidConfig(config) {
|
|
|
62
85
|
services.forEach(svc => {
|
|
63
86
|
if (!application.service.some(s => s.$['android:name'] === svc.name)) {
|
|
64
87
|
application.service.push({
|
|
65
|
-
$: { 'android:name': svc.name, 'android:exported':
|
|
88
|
+
$: { 'android:name': svc.name, 'android:exported': 'true', 'android:permission': svc.permission },
|
|
66
89
|
'intent-filter': [{ action: [{ $: { 'android:name': svc.action } }] }]
|
|
67
90
|
});
|
|
68
91
|
}
|
|
69
92
|
});
|
|
70
93
|
|
|
71
|
-
//
|
|
94
|
+
// Receivers
|
|
72
95
|
application.receiver = application.receiver || [];
|
|
73
96
|
if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
|
|
74
97
|
application.receiver.push({
|
|
@@ -80,7 +103,7 @@ function withAndroidConfig(config) {
|
|
|
80
103
|
});
|
|
81
104
|
}
|
|
82
105
|
|
|
83
|
-
/** IOS
|
|
106
|
+
/** 3. IOS CONFIG **/
|
|
84
107
|
function withIosConfig(config) {
|
|
85
108
|
return withInfoPlist(config, (config) => {
|
|
86
109
|
const infoPlist = config.modResults;
|
|
@@ -91,5 +114,5 @@ function withIosConfig(config) {
|
|
|
91
114
|
}
|
|
92
115
|
|
|
93
116
|
module.exports = (config) => {
|
|
94
|
-
return withPlugins(config, [withAndroidConfig, withIosConfig]);
|
|
117
|
+
return withPlugins(config, [withAndroidConfig, withMainActivityDataFix, withIosConfig]);
|
|
95
118
|
};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
package com.rnsnativecall
|
|
2
|
-
|
|
3
|
-
import android.app.NotificationManager
|
|
4
|
-
import android.content.Context
|
|
5
|
-
import android.os.Bundle
|
|
6
|
-
import android.view.View
|
|
7
|
-
import android.view.WindowManager
|
|
8
|
-
import android.widget.ImageView
|
|
9
|
-
import android.widget.TextView
|
|
10
|
-
import androidx.appcompat.app.AppCompatActivity
|
|
11
|
-
import com.bumptech.glide.Glide
|
|
12
|
-
|
|
13
|
-
class IncomingCallActivity : AppCompatActivity() {
|
|
14
|
-
|
|
15
|
-
override fun onCreate(savedInstanceState: Bundle?) {
|
|
16
|
-
super.onCreate(savedInstanceState)
|
|
17
|
-
|
|
18
|
-
// 1. Show over lockscreen
|
|
19
|
-
window.addFlags(
|
|
20
|
-
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
21
|
-
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
|
22
|
-
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
|
|
23
|
-
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
// 2. Set the layout FIRST
|
|
27
|
-
setContentView(R.layout.activity_incoming_call)
|
|
28
|
-
|
|
29
|
-
// 3. Extract data from Intent
|
|
30
|
-
val uuid = intent.getStringExtra("EXTRA_CALL_UUID")
|
|
31
|
-
val name = intent.getStringExtra("EXTRA_CALLER_NAME") ?: "Unknown"
|
|
32
|
-
val profileUrl = intent.getStringExtra("profile")
|
|
33
|
-
val callType = intent.getStringExtra("callType") ?: "audio"
|
|
34
|
-
|
|
35
|
-
// 4. Initialize Views
|
|
36
|
-
val imageView = findViewById<ImageView>(R.id.caller_image)
|
|
37
|
-
val nameText = findViewById<TextView>(R.id.caller_name)
|
|
38
|
-
val typeText = findViewById<TextView>(R.id.call_type)
|
|
39
|
-
|
|
40
|
-
nameText.text = name
|
|
41
|
-
typeText.text = if (callType == "video") "Incoming Video..." else "Incoming Audio..."
|
|
42
|
-
|
|
43
|
-
// 5. Load Image with Glide
|
|
44
|
-
if (!profileUrl.isNullOrEmpty()) {
|
|
45
|
-
Glide.with(this)
|
|
46
|
-
.load(profileUrl)
|
|
47
|
-
.circleCrop()
|
|
48
|
-
.into(imageView)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 6. Handle buttons
|
|
52
|
-
findViewById<View>(R.id.btn_accept).setOnClickListener {
|
|
53
|
-
dismissCall()
|
|
54
|
-
CallModule.sendEventToJS("onCallAccepted", mapOf("callUUID" to uuid))
|
|
55
|
-
finish()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
findViewById<View>(R.id.btn_reject).setOnClickListener {
|
|
59
|
-
dismissCall()
|
|
60
|
-
CallModule.sendEventToJS("onCallRejected", mapOf("callUUID" to uuid))
|
|
61
|
-
finish()
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private fun dismissCall() {
|
|
66
|
-
// This removes the "Pill" notification from the status bar
|
|
67
|
-
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
68
|
-
notificationManager.cancel(101) // Use the same ID you used in NativeCallManager
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
override fun onBackPressed() {
|
|
72
|
-
// Disable back button so they have to click Accept or Reject
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
-
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
4
|
-
android:layout_width="match_parent"
|
|
5
|
-
android:layout_height="match_parent"
|
|
6
|
-
android:background="#00000000">
|
|
7
|
-
<LinearLayout
|
|
8
|
-
android:layout_width="match_parent"
|
|
9
|
-
android:layout_height="wrap_content"
|
|
10
|
-
android:layout_marginHorizontal="8dp"
|
|
11
|
-
android:layout_marginTop="30dp"
|
|
12
|
-
android:background="@drawable/pill_background"
|
|
13
|
-
android:elevation="15dp"
|
|
14
|
-
android:orientation="horizontal"
|
|
15
|
-
android:padding="12dp"
|
|
16
|
-
android:gravity="center_vertical">
|
|
17
|
-
|
|
18
|
-
<com.google.android.material.imageview.ShapeableImageView
|
|
19
|
-
android:id="@+id/caller_image"
|
|
20
|
-
android:layout_width="50dp"
|
|
21
|
-
android:layout_height="50dp"
|
|
22
|
-
android:background="#ccc"
|
|
23
|
-
app:shapeAppearanceOverlay="@style/CircleImage" />
|
|
24
|
-
|
|
25
|
-
<LinearLayout
|
|
26
|
-
android:layout_width="0dp"
|
|
27
|
-
android:layout_height="wrap_content"
|
|
28
|
-
android:layout_weight="1"
|
|
29
|
-
android:layout_marginStart="15dp"
|
|
30
|
-
android:orientation="vertical">
|
|
31
|
-
|
|
32
|
-
<TextView
|
|
33
|
-
android:id="@+id/caller_name"
|
|
34
|
-
android:layout_width="wrap_content"
|
|
35
|
-
android:layout_height="wrap_content"
|
|
36
|
-
android:text="Caller Name"
|
|
37
|
-
android:textColor="#FFFFFF"
|
|
38
|
-
android:textSize="16sp"
|
|
39
|
-
android:textStyle="bold" />
|
|
40
|
-
|
|
41
|
-
<TextView
|
|
42
|
-
android:id="@+id/call_type"
|
|
43
|
-
android:layout_width="wrap_content"
|
|
44
|
-
android:layout_height="wrap_content"
|
|
45
|
-
android:text="Incoming Audio..."
|
|
46
|
-
android:textColor="#FFFFFF"
|
|
47
|
-
android:textSize="13sp" />
|
|
48
|
-
</LinearLayout>
|
|
49
|
-
|
|
50
|
-
<ImageButton
|
|
51
|
-
android:id="@+id/btn_accept"
|
|
52
|
-
android:layout_width="42dp"
|
|
53
|
-
android:layout_height="42dp"
|
|
54
|
-
android:background="@drawable/circle_green"
|
|
55
|
-
android:src="@android:drawable/ic_menu_call"
|
|
56
|
-
android:layout_marginEnd="12dp"
|
|
57
|
-
android:tint="#FFFFFF" />
|
|
58
|
-
|
|
59
|
-
<ImageButton
|
|
60
|
-
android:id="@+id/btn_reject"
|
|
61
|
-
android:layout_width="42dp"
|
|
62
|
-
android:layout_height="42dp"
|
|
63
|
-
android:background="@drawable/circle_red"
|
|
64
|
-
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
|
65
|
-
android:tint="#FFFFFF" />
|
|
66
|
-
</LinearLayout>
|
|
67
|
-
</FrameLayout>
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<resources>
|
|
3
|
-
<style name="CircleImage" parent="">
|
|
4
|
-
<item name="cornerSize">50%</item>
|
|
5
|
-
</style>
|
|
6
|
-
|
|
7
|
-
<style name="Theme.IncomingCall" parent="Theme.AppCompat.Light.NoActionBar">
|
|
8
|
-
<item name="android:windowBackground">@android:color/transparent</item>
|
|
9
|
-
<item name="android:windowIsTranslucent">true</item>
|
|
10
|
-
<item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
|
|
11
|
-
</style>
|
|
12
|
-
</resources>
|