rns-nativecall 0.1.5 → 0.1.7
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/build.gradle +10 -2
- package/android/src/main/java/com/rnsnativecall/AcceptCallActivity.kt +38 -0
- package/android/src/main/java/com/rnsnativecall/CallActionReceiver.kt +41 -0
- package/android/src/main/java/com/rnsnativecall/CallModule.kt +92 -63
- package/android/src/main/java/com/rnsnativecall/CallPackage.kt +3 -5
- package/android/src/main/java/com/rnsnativecall/IncomingCallActivity.kt +74 -0
- package/android/src/main/java/com/rnsnativecall/MyCallConnection.kt +24 -41
- package/android/src/main/java/com/rnsnativecall/MyConnectionService.kt +26 -42
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +71 -60
- package/android/src/main/res/drawable/circle_green.xml +4 -0
- package/android/src/main/res/drawable/circle_red.xml +4 -0
- package/android/src/main/res/drawable/pill_background.xml +5 -0
- package/android/src/main/res/layout/activity_incoming_call.xml +67 -0
- package/android/src/main/res/values/styles.xml +12 -0
- package/index.js +9 -8
- package/package.json +1 -1
- package/withNativeCallVoip.js +56 -63
package/android/build.gradle
CHANGED
|
@@ -48,7 +48,15 @@ dependencies {
|
|
|
48
48
|
// Kotlin Standard Library
|
|
49
49
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
// so no extra external dependencies are needed for them.
|
|
51
|
+
// Firebase for FCM
|
|
53
52
|
implementation "com.google.firebase:firebase-messaging:23.4.0"
|
|
53
|
+
|
|
54
|
+
// UI & Design (Crucial for the "Pill" Activity and Notifications)
|
|
55
|
+
implementation "com.google.android.material:material:1.9.0"
|
|
56
|
+
implementation "androidx.appcompat:appcompat:1.6.1"
|
|
57
|
+
implementation "androidx.core:core-ktx:1.10.1"
|
|
58
|
+
|
|
59
|
+
// Image Loading (To load the profile picture URL)
|
|
60
|
+
implementation "com.github.bumptech.glide:glide:4.15.1"
|
|
61
|
+
annotationProcessor "com.github.bumptech.glide:compiler:4.15.1"
|
|
54
62
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.rnsnativecall
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
|
|
9
|
+
class AcceptCallActivity : Activity() {
|
|
10
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
11
|
+
super.onCreate(savedInstanceState)
|
|
12
|
+
|
|
13
|
+
// 1. CLEAR THE PILL IMMEDIATELY
|
|
14
|
+
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
15
|
+
notificationManager.cancel(101)
|
|
16
|
+
|
|
17
|
+
// 2. PREPARE DATA FOR JS
|
|
18
|
+
val dataMap = mutableMapOf<String, String>()
|
|
19
|
+
intent.extras?.keySet()?.forEach { key ->
|
|
20
|
+
intent.getStringExtra(key)?.let { dataMap[key] = it }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 3. SEND EVENT TO REACT NATIVE
|
|
24
|
+
CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
25
|
+
|
|
26
|
+
// 4. OPEN THE MAIN APP
|
|
27
|
+
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
28
|
+
launchIntent?.apply {
|
|
29
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
30
|
+
putExtras(intent.extras ?: Bundle())
|
|
31
|
+
putExtra("navigatingToCall", true)
|
|
32
|
+
}
|
|
33
|
+
startActivity(launchIntent)
|
|
34
|
+
|
|
35
|
+
// 5. KILL THIS INVISIBLE WINDOW
|
|
36
|
+
finish()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package com.rnsnativecall
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.app.NotificationManager
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
|
|
9
|
+
class CallActionReceiver : BroadcastReceiver() {
|
|
10
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
11
|
+
val uuid = intent.getStringExtra("EXTRA_CALL_UUID")
|
|
12
|
+
|
|
13
|
+
// 1. Clear the notification
|
|
14
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
15
|
+
notificationManager.cancel(101)
|
|
16
|
+
|
|
17
|
+
if (intent.action == "ACTION_ACCEPT") {
|
|
18
|
+
// 2. Prepare the data for React Native
|
|
19
|
+
val dataMap = mutableMapOf<String, String>()
|
|
20
|
+
intent.extras?.keySet()?.forEach { key ->
|
|
21
|
+
intent.getStringExtra(key)?.let { dataMap[key] = it }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 3. Send the event to JS if the app is already running in background
|
|
25
|
+
CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
26
|
+
|
|
27
|
+
// 4. Bring the app to foreground
|
|
28
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
29
|
+
launchIntent?.apply {
|
|
30
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
31
|
+
// Forward the extras so the App can read them on startup/resume
|
|
32
|
+
putExtras(intent.extras ?: Bundle())
|
|
33
|
+
putExtra("navigatingToCall", true)
|
|
34
|
+
}
|
|
35
|
+
context.startActivity(launchIntent)
|
|
36
|
+
} else {
|
|
37
|
+
// Logic for Reject
|
|
38
|
+
CallModule.sendEventToJS("onCallRejected", mapOf("callUUID" to uuid))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/// Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/CallModule.kt
|
|
2
1
|
package com.rnsnativecall
|
|
3
2
|
|
|
4
3
|
import android.content.ComponentName
|
|
@@ -12,6 +11,9 @@ import android.telecom.PhoneAccountHandle
|
|
|
12
11
|
import android.telecom.TelecomManager
|
|
13
12
|
import com.facebook.react.bridge.*
|
|
14
13
|
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
|
|
15
17
|
|
|
16
18
|
class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
17
19
|
|
|
@@ -24,62 +26,47 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
24
26
|
|
|
25
27
|
private fun getPhoneAccountHandle(): PhoneAccountHandle {
|
|
26
28
|
val componentName = ComponentName(reactApplicationContext, MyConnectionService::class.java)
|
|
27
|
-
// Using the app's package name for the ID makes it unique for every app using this package
|
|
28
29
|
return PhoneAccountHandle(componentName, "${reactApplicationContext.packageName}.voip")
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
private fun registerPhoneAccount() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.build()
|
|
54
|
-
|
|
55
|
-
telecomManager.registerPhoneAccount(phoneAccount)
|
|
56
|
-
}
|
|
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
|
+
}
|
|
57
54
|
|
|
58
55
|
@ReactMethod
|
|
59
|
-
fun displayIncomingCall(
|
|
60
|
-
|
|
61
|
-
number: String,
|
|
62
|
-
name: String,
|
|
63
|
-
hasVideo: Boolean,
|
|
64
|
-
playRing: Boolean,
|
|
65
|
-
promise: Promise
|
|
66
|
-
) {
|
|
67
|
-
val telecomManager =
|
|
68
|
-
reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
56
|
+
fun displayIncomingCall(uuid: String, number: String, name: String, hasVideo: Boolean, playRing: Boolean, promise: Promise) {
|
|
57
|
+
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
69
58
|
val phoneAccountHandle = getPhoneAccountHandle()
|
|
70
59
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
putBoolean("EXTRA_PLAY_RING", playRing)
|
|
80
|
-
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, hasVideo)
|
|
81
|
-
}
|
|
60
|
+
val extras = Bundle().apply {
|
|
61
|
+
putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, Uri.fromParts("sip", number, null))
|
|
62
|
+
putString("EXTRA_CALL_UUID", uuid)
|
|
63
|
+
|
|
64
|
+
putString("EXTRA_CALLER_NAME", name)
|
|
65
|
+
putString(TelecomManager.EXTRA_CALL_SUBJECT, number)
|
|
66
|
+
putBoolean("EXTRA_PLAY_RING", false)
|
|
67
|
+
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, hasVideo)
|
|
82
68
|
|
|
69
|
+
}
|
|
83
70
|
try {
|
|
84
71
|
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
85
72
|
promise.resolve(true)
|
|
@@ -98,23 +85,49 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
98
85
|
}
|
|
99
86
|
}
|
|
100
87
|
|
|
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
|
+
|
|
101
95
|
@ReactMethod
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
fun checkTelecomPermissions(promise: Promise) {
|
|
97
|
+
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
98
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
99
|
+
val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
|
|
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)
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
112
120
|
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
|
|
113
121
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
114
122
|
reactApplicationContext.startActivity(intent)
|
|
123
|
+
|
|
115
124
|
promise.resolve(false)
|
|
125
|
+
} catch (e: Exception) {
|
|
126
|
+
promise.reject("PERMISSION_ERROR", "Could not open settings: ${e.message}")
|
|
116
127
|
}
|
|
117
128
|
}
|
|
129
|
+
}
|
|
130
|
+
|
|
118
131
|
|
|
119
132
|
@ReactMethod fun addListener(eventName: String) {}
|
|
120
133
|
@ReactMethod fun removeListeners(count: Int) {}
|
|
@@ -122,11 +135,27 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
122
135
|
companion object {
|
|
123
136
|
private var instance: CallModule? = null
|
|
124
137
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
instance?.reactApplicationContext
|
|
128
|
-
|
|
129
|
-
|
|
138
|
+
@JvmStatic
|
|
139
|
+
fun sendEventToJS(eventName: String, params: Any?) {
|
|
140
|
+
val reactContext = instance?.reactApplicationContext ?: return
|
|
141
|
+
|
|
142
|
+
val bridgeData = when (params) {
|
|
143
|
+
is Map<*, *> -> {
|
|
144
|
+
val map = Arguments.createMap()
|
|
145
|
+
params.forEach { (key, value) ->
|
|
146
|
+
map.putString(key.toString(), value.toString())
|
|
147
|
+
}
|
|
148
|
+
map
|
|
149
|
+
}
|
|
150
|
+
is String -> {
|
|
151
|
+
Arguments.createMap().apply { putString("callUUID", params) }
|
|
152
|
+
}
|
|
153
|
+
else -> null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
reactContext
|
|
157
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
158
|
+
?.emit(eventName, bridgeData)
|
|
130
159
|
}
|
|
131
160
|
}
|
|
132
|
-
}
|
|
161
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/////
|
|
1
|
+
/////Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/CallPackage.kt
|
|
2
2
|
package com.rnsnativecall
|
|
3
3
|
|
|
4
4
|
import com.facebook.react.ReactPackage
|
|
@@ -11,9 +11,7 @@ class CallPackage : ReactPackage {
|
|
|
11
11
|
return listOf(CallModule(reactContext))
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
override fun createViewManagers(
|
|
15
|
-
reactContext: ReactApplicationContext
|
|
16
|
-
): List<ViewManager<*, *>> {
|
|
14
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
17
15
|
return emptyList()
|
|
18
16
|
}
|
|
19
|
-
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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,5 +1,3 @@
|
|
|
1
|
-
/////
|
|
2
|
-
// Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/MyCallConnection.kt
|
|
3
1
|
package com.rnsnativecall
|
|
4
2
|
|
|
5
3
|
import android.content.Context
|
|
@@ -11,11 +9,12 @@ import android.telecom.Connection
|
|
|
11
9
|
import android.telecom.DisconnectCause
|
|
12
10
|
|
|
13
11
|
class MyCallConnection(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
private val context: Context,
|
|
13
|
+
private val callUUID: String?,
|
|
14
|
+
private val playRing: Boolean,
|
|
15
|
+
private val allData: Map<String, String>,
|
|
16
|
+
private val onAcceptCallback: (Map<String, String>) -> Unit,
|
|
17
|
+
private val onRejectCallback: (Map<String, String>) -> Unit
|
|
19
18
|
) : Connection() {
|
|
20
19
|
|
|
21
20
|
private var mediaPlayer: MediaPlayer? = null
|
|
@@ -23,28 +22,22 @@ class MyCallConnection(
|
|
|
23
22
|
init {
|
|
24
23
|
connectionProperties = PROPERTY_SELF_MANAGED
|
|
25
24
|
audioModeIsVoip = true
|
|
26
|
-
|
|
27
|
-
if (playRing) {
|
|
28
|
-
startRingtone()
|
|
29
|
-
}
|
|
25
|
+
if (playRing) startRingtone()
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
private fun startRingtone() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
prepare()
|
|
46
|
-
start()
|
|
47
|
-
}
|
|
29
|
+
try {
|
|
30
|
+
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
31
|
+
mediaPlayer = MediaPlayer().apply {
|
|
32
|
+
setDataSource(context, uri)
|
|
33
|
+
setAudioAttributes(AudioAttributes.Builder()
|
|
34
|
+
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
35
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build())
|
|
36
|
+
isLooping = true
|
|
37
|
+
prepare()
|
|
38
|
+
start()
|
|
39
|
+
}
|
|
40
|
+
} catch (e: Exception) { e.printStackTrace() }
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
private fun stopRingtone() {
|
|
@@ -60,18 +53,15 @@ class MyCallConnection(
|
|
|
60
53
|
|
|
61
54
|
override fun onAnswer() {
|
|
62
55
|
stopRingtone()
|
|
63
|
-
|
|
64
|
-
// Bring app to foreground
|
|
65
56
|
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
66
57
|
launchIntent?.apply {
|
|
67
58
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
68
59
|
context.startActivity(this)
|
|
69
60
|
}
|
|
70
61
|
|
|
71
|
-
|
|
72
|
-
onAcceptCallback(callUUID)
|
|
62
|
+
onAcceptCallback(allData)
|
|
73
63
|
|
|
74
|
-
//
|
|
64
|
+
// IMPORTANT: KEEP THIS AS IT IS
|
|
75
65
|
setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
|
|
76
66
|
cleanUp()
|
|
77
67
|
destroy()
|
|
@@ -82,7 +72,7 @@ class MyCallConnection(
|
|
|
82
72
|
setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
|
|
83
73
|
cleanUp()
|
|
84
74
|
destroy()
|
|
85
|
-
onRejectCallback(
|
|
75
|
+
onRejectCallback(allData)
|
|
86
76
|
}
|
|
87
77
|
|
|
88
78
|
override fun onDisconnect() {
|
|
@@ -90,13 +80,6 @@ class MyCallConnection(
|
|
|
90
80
|
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
91
81
|
cleanUp()
|
|
92
82
|
destroy()
|
|
93
|
-
onRejectCallback(
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
override fun onAbort() {
|
|
97
|
-
stopRingtone()
|
|
98
|
-
setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
|
|
99
|
-
cleanUp()
|
|
100
|
-
destroy()
|
|
83
|
+
onRejectCallback(allData)
|
|
101
84
|
}
|
|
102
|
-
}
|
|
85
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
|
-
import android.net.Uri
|
|
3
|
+
import android.net.Uri
|
|
4
4
|
import android.telecom.Connection
|
|
5
5
|
import android.telecom.ConnectionRequest
|
|
6
6
|
import android.telecom.ConnectionService
|
|
@@ -12,66 +12,50 @@ class MyConnectionService : ConnectionService() {
|
|
|
12
12
|
|
|
13
13
|
companion object {
|
|
14
14
|
private val activeConnections = ConcurrentHashMap<String, MyCallConnection>()
|
|
15
|
-
|
|
16
15
|
fun getConnection(uuid: String) = activeConnections[uuid]
|
|
17
|
-
fun addConnection(uuid: String, connection: MyCallConnection) {
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
fun removeConnection(uuid: String) {
|
|
21
|
-
activeConnections.remove(uuid)
|
|
22
|
-
}
|
|
16
|
+
fun addConnection(uuid: String, connection: MyCallConnection) { activeConnections[uuid] = connection }
|
|
17
|
+
fun removeConnection(uuid: String) { activeConnections.remove(uuid) }
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
override fun onCreateIncomingConnection(
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
connectionManagerPhoneAccount: PhoneAccountHandle?,
|
|
22
|
+
request: ConnectionRequest?
|
|
28
23
|
): Connection {
|
|
29
|
-
|
|
30
24
|
val extras = request?.extras
|
|
31
25
|
val callUUID = extras?.getString("EXTRA_CALL_UUID")
|
|
32
26
|
val playRing = extras?.getBoolean("EXTRA_PLAY_RING", true) ?: true
|
|
27
|
+
|
|
28
|
+
// Convert Bundle to Map to send to JS
|
|
29
|
+
val extrasMap = mutableMapOf<String, String>()
|
|
30
|
+
extras?.keySet()?.forEach { key ->
|
|
31
|
+
val value = extras.get(key)
|
|
32
|
+
if (value is String) extrasMap[key] = value
|
|
33
|
+
}
|
|
33
34
|
|
|
34
|
-
// 1. "John Doe" (Top slot)
|
|
35
35
|
val actualPersonName = extras?.getString("EXTRA_CALLER_NAME") ?: "Someone"
|
|
36
|
-
|
|
37
|
-
// 2. "Raiidr Video Call" (Middle/Number slot)
|
|
38
36
|
val appLabel = extras?.getString(TelecomManager.EXTRA_CALL_SUBJECT) ?: "Incoming Call"
|
|
39
37
|
|
|
40
|
-
val connection =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
onRejectCallback = { uuid ->
|
|
49
|
-
CallModule.sendEventToJS("onCallRejected", uuid)
|
|
50
|
-
}
|
|
51
|
-
)
|
|
38
|
+
val connection = MyCallConnection(
|
|
39
|
+
context = applicationContext,
|
|
40
|
+
callUUID = callUUID,
|
|
41
|
+
playRing = playRing,
|
|
42
|
+
allData = extrasMap,
|
|
43
|
+
onAcceptCallback = { dataMap -> CallModule.sendEventToJS("onCallAccepted", dataMap) },
|
|
44
|
+
onRejectCallback = { dataMap -> CallModule.sendEventToJS("onCallRejected", dataMap) }
|
|
45
|
+
)
|
|
52
46
|
|
|
53
|
-
// 3. Set the LARGE text on top to the Person's Name
|
|
54
47
|
connection.setCallerDisplayName(actualPersonName, TelecomManager.PRESENTATION_ALLOWED)
|
|
55
|
-
|
|
56
|
-
// 4. Standard Telecom setup
|
|
57
|
-
connection.connectionCapabilities =
|
|
58
|
-
Connection.CAPABILITY_MUTE or Connection.CAPABILITY_SUPPORT_HOLD
|
|
59
|
-
|
|
60
|
-
// --- THE UI FIX ---
|
|
61
|
-
// We force the App Label (e.g. "Raiidr Video Call") into the Address slot
|
|
62
|
-
// This replaces the raw phone number/handle on the incoming call screen.
|
|
63
48
|
val labelUri = Uri.fromParts("tel", appLabel, null)
|
|
64
49
|
connection.setAddress(labelUri, TelecomManager.PRESENTATION_ALLOWED)
|
|
65
|
-
|
|
50
|
+
|
|
66
51
|
connection.setInitializing()
|
|
67
52
|
connection.setRinging()
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
callUUID?.let { addConnection(it, connection) }
|
|
54
|
+
callUUID?.let {
|
|
55
|
+
addConnection(it, connection)
|
|
56
|
+
CallModule.sendEventToJS("onCallDisplayed", extrasMap)
|
|
57
|
+
}
|
|
74
58
|
|
|
75
59
|
return connection
|
|
76
60
|
}
|
|
77
|
-
}
|
|
61
|
+
}
|
|
@@ -1,78 +1,89 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
|
-
import android.
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.app.PendingIntent
|
|
4
6
|
import android.content.Context
|
|
5
|
-
import android.
|
|
6
|
-
import android.os.
|
|
7
|
-
import
|
|
8
|
-
import android.telecom.TelecomManager
|
|
9
|
-
import android.telecom.VideoProfile
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import androidx.core.app.NotificationCompat
|
|
10
10
|
|
|
11
11
|
object NativeCallManager {
|
|
12
12
|
|
|
13
13
|
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
14
14
|
val uuid = data["callId"] ?: return
|
|
15
|
-
val
|
|
15
|
+
val name = data["name"] ?: "Incoming Call"
|
|
16
16
|
val callType = data["callType"] ?: "audio"
|
|
17
|
-
val name = data["name"] ?: handle
|
|
18
|
-
val playRing = data["playRing"]?.toBoolean() ?: true
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
pm.getApplicationLabel(ai).toString()
|
|
26
|
-
} catch (e: Exception) {
|
|
27
|
-
"App"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// 2. Format Sub-label (e.g., "Raiidr Video Call")
|
|
31
|
-
val capitalizedCallType =
|
|
32
|
-
callType.replaceFirstChar {
|
|
33
|
-
if (it.isLowerCase()) it.titlecase() else it.toString()
|
|
34
|
-
}
|
|
35
|
-
val subLabel = "$appName $capitalizedCallType Call"
|
|
36
|
-
|
|
37
|
-
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
38
|
-
val phoneAccountHandle = getPhoneAccountHandle(context)
|
|
39
|
-
|
|
40
|
-
val extras =
|
|
41
|
-
Bundle().apply {
|
|
42
|
-
putParcelable(
|
|
43
|
-
TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
|
|
44
|
-
Uri.fromParts("sip", handle, null)
|
|
45
|
-
)
|
|
46
|
-
putString("EXTRA_CALL_UUID", uuid)
|
|
47
|
-
|
|
48
|
-
// This goes to the SECONDARY slot in MyConnectionService
|
|
49
|
-
putString(TelecomManager.EXTRA_CALL_SUBJECT, subLabel)
|
|
18
|
+
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
19
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
20
|
+
} else {
|
|
21
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
22
|
+
}
|
|
50
23
|
|
|
51
|
-
|
|
52
|
-
|
|
24
|
+
// 1. DUMMY INTENT: This is the key to making the Pill "Sticky".
|
|
25
|
+
// It prevents the notification from disappearing but doesn't open a screen.
|
|
26
|
+
val dummyIntent = PendingIntent.getActivity(
|
|
27
|
+
context,
|
|
28
|
+
0,
|
|
29
|
+
Intent(),
|
|
30
|
+
pendingFlags
|
|
31
|
+
)
|
|
53
32
|
|
|
54
|
-
|
|
33
|
+
// 2. Accept Action
|
|
34
|
+
val acceptIntent = Intent(context, AcceptCallActivity::class.java).apply {
|
|
35
|
+
action = "ACTION_ACCEPT"
|
|
36
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
37
|
+
data.forEach { (key, value) -> putExtra(key, value) }
|
|
38
|
+
// Crucial for launching from a notification
|
|
39
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
40
|
+
}
|
|
55
41
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
42
|
+
// Keep this as getActivity
|
|
43
|
+
val acceptPendingIntent = PendingIntent.getActivity(
|
|
44
|
+
context,
|
|
45
|
+
1001,
|
|
46
|
+
acceptIntent,
|
|
47
|
+
pendingFlags
|
|
48
|
+
)
|
|
49
|
+
// 3. Reject Action
|
|
50
|
+
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
51
|
+
action = "ACTION_REJECT"
|
|
52
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
53
|
+
}
|
|
54
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(context, 1002, rejectIntent, pendingFlags)
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
// 4. Setup Channel (Ensure Importance is HIGH)
|
|
57
|
+
val channelId = "CALL_CHANNEL_ID"
|
|
58
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
61
|
+
val channel = NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
62
|
+
description = "Shows incoming call notifications"
|
|
63
|
+
enableVibration(true)
|
|
64
|
+
// Critical for bypassing "Do Not Disturb"
|
|
65
|
+
setBypassDnd(true)
|
|
66
|
+
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
67
|
+
}
|
|
68
|
+
notificationManager.createNotificationChannel(channel)
|
|
70
69
|
}
|
|
71
|
-
}
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
// 5. Build the Notification
|
|
72
|
+
val builder = NotificationCompat.Builder(context, channelId)
|
|
73
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
74
|
+
.setContentTitle("Incoming $callType call")
|
|
75
|
+
.setContentText(name)
|
|
76
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
77
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
78
|
+
.setOngoing(true) // Prevents user from swiping it away
|
|
79
|
+
.setAutoCancel(false)
|
|
80
|
+
|
|
81
|
+
// This is what keeps the Heads-Up "Pill" visible indefinitely:
|
|
82
|
+
.setFullScreenIntent(dummyIntent, true)
|
|
83
|
+
|
|
84
|
+
.addAction(0, "Answer", acceptPendingIntent)
|
|
85
|
+
.addAction(0, "Decline", rejectPendingIntent)
|
|
86
|
+
|
|
87
|
+
notificationManager.notify(101, builder.build())
|
|
77
88
|
}
|
|
78
|
-
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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>
|
|
@@ -0,0 +1,12 @@
|
|
|
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>
|
package/index.js
CHANGED
|
@@ -26,6 +26,11 @@ const REQUIRED_PERMISSIONS = Platform.OS === 'android' ? [
|
|
|
26
26
|
export async function ensureAndroidPermissions() {
|
|
27
27
|
if (Platform.OS !== 'android') return true;
|
|
28
28
|
const result = await PermissionsAndroid.requestMultiple(REQUIRED_PERMISSIONS);
|
|
29
|
+
|
|
30
|
+
// This triggers the "Calling Accounts" system settings if not enabled
|
|
31
|
+
const isAccountEnabled = await CallModule.checkTelecomPermissions();
|
|
32
|
+
if (!isAccountEnabled) return false;
|
|
33
|
+
|
|
29
34
|
return Object.values(result).every(status => status === PermissionsAndroid.RESULTS.GRANTED);
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -33,14 +38,10 @@ export const CallHandler = {
|
|
|
33
38
|
displayCall: async (uuid, number, name, hasVideo = false, shouldRing = true) => {
|
|
34
39
|
if (!CallModule) return false;
|
|
35
40
|
|
|
36
|
-
if (Platform.OS === 'android') {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// This triggers the "Calling Accounts" system settings if not enabled
|
|
41
|
-
const isAccountEnabled = await CallModule.checkTelecomPermissions();
|
|
42
|
-
if (!isAccountEnabled) return false;
|
|
43
|
-
}
|
|
41
|
+
// if (Platform.OS === 'android') {
|
|
42
|
+
// const hasPerms = await ensureAndroidPermissions();
|
|
43
|
+
// if (!hasPerms) return false;
|
|
44
|
+
// }
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
47
|
return await CallModule.displayIncomingCall(
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -1,65 +1,78 @@
|
|
|
1
1
|
const { withAndroidManifest, withInfoPlist, withPlugins } = require('@expo/config-plugins');
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* ANDROID CONFIGURATION
|
|
5
|
-
*/
|
|
6
3
|
function withAndroidConfig(config) {
|
|
7
4
|
return withAndroidManifest(config, (config) => {
|
|
8
5
|
const manifest = config.modResults;
|
|
6
|
+
const application = manifest.manifest.application[0];
|
|
9
7
|
|
|
8
|
+
// 1. Permissions
|
|
10
9
|
const permissions = [
|
|
11
10
|
'android.permission.READ_PHONE_NUMBERS',
|
|
12
11
|
'android.permission.CALL_PHONE',
|
|
13
|
-
'android.permission.MANAGE_OWN_CALLS'
|
|
12
|
+
'android.permission.MANAGE_OWN_CALLS',
|
|
13
|
+
'android.permission.USE_FULL_SCREEN_INTENT',
|
|
14
|
+
'android.permission.VIBRATE',
|
|
15
|
+
'android.permission.FOREGROUND_SERVICE',
|
|
16
|
+
'android.permission.FOREGROUND_SERVICE_PHONE_CALL'
|
|
14
17
|
];
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
manifest.manifest['uses-permission'] = [];
|
|
18
|
-
}
|
|
19
|
-
|
|
19
|
+
manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
|
|
20
20
|
permissions.forEach((perm) => {
|
|
21
21
|
if (!manifest.manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
|
|
22
22
|
manifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
application.
|
|
28
|
-
|
|
29
|
-
// MyConnectionService
|
|
30
|
-
const connectionServiceName = 'com.rnsnativecall.MyConnectionService';
|
|
31
|
-
const existingConnectionService = application.service.find((s) => s.$['android:name'] === connectionServiceName);
|
|
26
|
+
// 2. Activities (UI Components)
|
|
27
|
+
application.activity = application.activity || [];
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
// IncomingCallActivity (Optional lock screen UI)
|
|
30
|
+
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.IncomingCallActivity')) {
|
|
31
|
+
application.activity.push({
|
|
35
32
|
$: {
|
|
36
|
-
'android:name':
|
|
37
|
-
'android:
|
|
38
|
-
'android:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{
|
|
42
|
-
action: [{ $: { 'android:name': 'android.telecom.ConnectionService' } }]
|
|
43
|
-
}
|
|
44
|
-
]
|
|
33
|
+
'android:name': 'com.rnsnativecall.IncomingCallActivity',
|
|
34
|
+
'android:showOnLockScreen': 'true',
|
|
35
|
+
'android:launchMode': 'singleInstance',
|
|
36
|
+
'android:theme': '@style/Theme.AppCompat.Light.NoActionBar'
|
|
37
|
+
}
|
|
45
38
|
});
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!existingFCMService) {
|
|
53
|
-
application.service.push({
|
|
41
|
+
// AcceptCallActivity (The "Trampoline" that fixes the Answer button)
|
|
42
|
+
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
|
|
43
|
+
application.activity.push({
|
|
54
44
|
$: {
|
|
55
|
-
'android:name':
|
|
45
|
+
'android:name': 'com.rnsnativecall.AcceptCallActivity',
|
|
46
|
+
'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
|
|
47
|
+
'android:excludeFromRecents': 'true',
|
|
48
|
+
'android:noHistory': 'true',
|
|
56
49
|
'android:exported': 'false',
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
'android:launchMode': 'singleInstance'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. Services (FCM and Telecom)
|
|
56
|
+
application.service = application.service || [];
|
|
57
|
+
const services = [
|
|
58
|
+
{ name: 'com.rnsnativecall.MyConnectionService', permission: 'android.permission.BIND_CONNECTION_SERVICE', action: 'android.telecom.ConnectionService' },
|
|
59
|
+
{ name: 'com.rnsnativecall.CallMessagingService', action: 'com.google.firebase.MESSAGING_EVENT' }
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
services.forEach(svc => {
|
|
63
|
+
if (!application.service.some(s => s.$['android:name'] === svc.name)) {
|
|
64
|
+
application.service.push({
|
|
65
|
+
$: { 'android:name': svc.name, 'android:exported': svc.permission ? 'true' : 'false', 'android:permission': svc.permission },
|
|
66
|
+
'intent-filter': [{ action: [{ $: { 'android:name': svc.action } }] }]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 4. Receivers (The Decline Button)
|
|
72
|
+
application.receiver = application.receiver || [];
|
|
73
|
+
if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
|
|
74
|
+
application.receiver.push({
|
|
75
|
+
$: { 'android:name': 'com.rnsnativecall.CallActionReceiver', 'android:exported': 'false' }
|
|
63
76
|
});
|
|
64
77
|
}
|
|
65
78
|
|
|
@@ -67,36 +80,16 @@ function withAndroidConfig(config) {
|
|
|
67
80
|
});
|
|
68
81
|
}
|
|
69
82
|
|
|
70
|
-
/**
|
|
71
|
-
* IOS CONFIGURATION
|
|
72
|
-
*/
|
|
83
|
+
/** IOS Config remains the same **/
|
|
73
84
|
function withIosConfig(config) {
|
|
74
85
|
return withInfoPlist(config, (config) => {
|
|
75
86
|
const infoPlist = config.modResults;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (!infoPlist.UIBackgroundModes) {
|
|
79
|
-
infoPlist.UIBackgroundModes = [];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
['voip', 'audio'].forEach((mode) => {
|
|
83
|
-
if (!infoPlist.UIBackgroundModes.includes(mode)) {
|
|
84
|
-
infoPlist.UIBackgroundModes.push(mode);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// 2. Dynamic Microphone Description
|
|
89
|
-
const appName = config.name || 'this app';
|
|
90
|
-
infoPlist.NSMicrophoneUsageDescription =
|
|
91
|
-
infoPlist.NSMicrophoneUsageDescription || `Allow ${appName} to access your microphone for calls.`;
|
|
92
|
-
|
|
87
|
+
if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
|
|
88
|
+
['voip', 'audio'].forEach(mode => { if (!infoPlist.UIBackgroundModes.includes(mode)) infoPlist.UIBackgroundModes.push(mode); });
|
|
93
89
|
return config;
|
|
94
90
|
});
|
|
95
91
|
}
|
|
96
92
|
|
|
97
93
|
module.exports = (config) => {
|
|
98
|
-
return withPlugins(config, [
|
|
99
|
-
|
|
100
|
-
withIosConfig,
|
|
101
|
-
]);
|
|
102
|
-
};
|
|
94
|
+
return withPlugins(config, [withAndroidConfig, withIosConfig]);
|
|
95
|
+
};
|