rns-nativecall 0.1.5 → 0.1.6
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/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 +36 -59
- 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 +3 -0
- package/index.js +5 -4
- package/package.json +1 -1
- package/withNativeCallVoip.js +46 -39
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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -10,69 +10,46 @@ import android.telecom.VideoProfile
|
|
|
10
10
|
|
|
11
11
|
object NativeCallManager {
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
val ai = pm.getApplicationInfo(context.packageName, 0)
|
|
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)
|
|
50
|
-
|
|
51
|
-
// This goes to the PRIMARY slot (The Name) in MyConnectionService
|
|
52
|
-
putString("EXTRA_CALLER_NAME", name)
|
|
53
|
-
|
|
54
|
-
putBoolean("EXTRA_PLAY_RING", playRing)
|
|
55
|
-
|
|
56
|
-
val isVideo = callType.equals("video", ignoreCase = true)
|
|
57
|
-
val videoState =
|
|
58
|
-
if (isVideo) VideoProfile.STATE_BIDIRECTIONAL
|
|
59
|
-
else VideoProfile.STATE_AUDIO_ONLY
|
|
60
|
-
putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState)
|
|
61
|
-
|
|
62
|
-
// Forward all other data
|
|
63
|
-
data.forEach { (key, value) -> putString(key, value) }
|
|
64
|
-
}
|
|
13
|
+
// Inside NativeCallManager.kt
|
|
14
|
+
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
15
|
+
val uuid = data["callId"] ?: ""
|
|
16
|
+
val name = data["name"] ?: "Unknown"
|
|
17
|
+
|
|
18
|
+
// Create the Intent for the FullScreen Activity
|
|
19
|
+
val fullScreenIntent = Intent(context, IncomingCallActivity::class.java).apply {
|
|
20
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION
|
|
21
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
22
|
+
putExtra("EXTRA_CALLER_NAME", name)
|
|
23
|
+
}
|
|
65
24
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
25
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0,
|
|
26
|
+
fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
27
|
+
|
|
28
|
+
// Build the Notification
|
|
29
|
+
val notificationBuilder = NotificationCompat.Builder(context, "CALL_CHANNEL_ID")
|
|
30
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
31
|
+
.setContentTitle("Incoming Call")
|
|
32
|
+
.setContentText(name)
|
|
33
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
34
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
35
|
+
.setFullScreenIntent(fullScreenPendingIntent, true) // High Priority HUN
|
|
36
|
+
.setOngoing(true)
|
|
37
|
+
.setAutoCancel(false)
|
|
38
|
+
|
|
39
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
40
|
+
|
|
41
|
+
// Create Channel for Android O+
|
|
42
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
43
|
+
val channel = NotificationChannel("CALL_CHANNEL_ID", "Incoming Calls", NotificationManager.IMPORTANCE_HIGH)
|
|
44
|
+
notificationManager.createNotificationChannel(channel)
|
|
71
45
|
}
|
|
72
46
|
|
|
73
|
-
|
|
47
|
+
notificationManager.notify(101, notificationBuilder.build())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
74
51
|
private fun getPhoneAccountHandle(context: Context): PhoneAccountHandle {
|
|
75
52
|
val componentName = ComponentName(context, MyConnectionService::class.java)
|
|
76
53
|
return PhoneAccountHandle(componentName, "${context.packageName}.voip")
|
|
77
54
|
}
|
|
78
|
-
}
|
|
55
|
+
}
|
|
@@ -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>
|
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
|
|
|
@@ -36,10 +41,6 @@ export const CallHandler = {
|
|
|
36
41
|
if (Platform.OS === 'android') {
|
|
37
42
|
const hasPerms = await ensureAndroidPermissions();
|
|
38
43
|
if (!hasPerms) return false;
|
|
39
|
-
|
|
40
|
-
// This triggers the "Calling Accounts" system settings if not enabled
|
|
41
|
-
const isAccountEnabled = await CallModule.checkTelecomPermissions();
|
|
42
|
-
if (!isAccountEnabled) return false;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
try {
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -6,62 +6,69 @@ const { withAndroidManifest, withInfoPlist, withPlugins } = require('@expo/confi
|
|
|
6
6
|
function withAndroidConfig(config) {
|
|
7
7
|
return withAndroidManifest(config, (config) => {
|
|
8
8
|
const manifest = config.modResults;
|
|
9
|
+
const application = manifest.manifest.application[0];
|
|
9
10
|
|
|
11
|
+
// 1. Unified Permissions List
|
|
10
12
|
const permissions = [
|
|
11
13
|
'android.permission.READ_PHONE_NUMBERS',
|
|
12
14
|
'android.permission.CALL_PHONE',
|
|
13
|
-
'android.permission.MANAGE_OWN_CALLS'
|
|
15
|
+
'android.permission.MANAGE_OWN_CALLS',
|
|
16
|
+
'android.permission.USE_FULL_SCREEN_INTENT',
|
|
17
|
+
'android.permission.VIBRATE',
|
|
18
|
+
'android.permission.FOREGROUND_SERVICE',
|
|
19
|
+
'android.permission.FOREGROUND_SERVICE_PHONE_CALL'
|
|
14
20
|
];
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
manifest.manifest['uses-permission'] = [];
|
|
18
|
-
}
|
|
19
|
-
|
|
22
|
+
manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
|
|
20
23
|
permissions.forEach((perm) => {
|
|
21
24
|
if (!manifest.manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
|
|
22
25
|
manifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
|
|
23
26
|
}
|
|
24
27
|
});
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
application.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const existingConnectionService = application.service.find((s) => s.$['android:name'] === connectionServiceName);
|
|
32
|
-
|
|
33
|
-
if (!existingConnectionService) {
|
|
34
|
-
application.service.push({
|
|
29
|
+
// 2. Register IncomingCallActivity
|
|
30
|
+
application.activity = application.activity || [];
|
|
31
|
+
const activityName = 'com.rnsnativecall.IncomingCallActivity';
|
|
32
|
+
if (!application.activity.some(a => a.$['android:name'] === activityName)) {
|
|
33
|
+
application.activity.push({
|
|
35
34
|
$: {
|
|
36
|
-
'android:name':
|
|
37
|
-
'android:
|
|
38
|
-
'android:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
]
|
|
35
|
+
'android:name': activityName,
|
|
36
|
+
'android:showOnLockScreen': 'true',
|
|
37
|
+
'android:launchMode': 'singleInstance',
|
|
38
|
+
'android:excludeFromRecents': 'true',
|
|
39
|
+
'android:screenOrientation': 'portrait',
|
|
40
|
+
'android:theme': '@style/Theme.AppCompat.Light.NoActionBar'
|
|
41
|
+
}
|
|
45
42
|
});
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
// 3. Services (ConnectionService & FCM)
|
|
46
|
+
application.service = application.service || [];
|
|
47
|
+
|
|
48
|
+
const services = [
|
|
49
|
+
{
|
|
50
|
+
name: 'com.rnsnativecall.MyConnectionService',
|
|
51
|
+
permission: 'android.permission.BIND_CONNECTION_SERVICE',
|
|
52
|
+
action: 'android.telecom.ConnectionService',
|
|
53
|
+
exported: 'true'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'com.rnsnativecall.CallMessagingService',
|
|
57
|
+
action: 'com.google.firebase.MESSAGING_EVENT',
|
|
58
|
+
exported: 'false'
|
|
59
|
+
}
|
|
60
|
+
];
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
application.service.
|
|
54
|
-
|
|
55
|
-
'android:name':
|
|
56
|
-
'android:
|
|
57
|
-
}
|
|
58
|
-
'
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
]
|
|
63
|
-
});
|
|
64
|
-
}
|
|
62
|
+
services.forEach(svc => {
|
|
63
|
+
if (!application.service.some(s => s.$['android:name'] === svc.name)) {
|
|
64
|
+
const serviceObj = {
|
|
65
|
+
$: { 'android:name': svc.name, 'android:exported': svc.exported },
|
|
66
|
+
'intent-filter': [{ action: [{ $: { 'android:name': svc.action } }] }]
|
|
67
|
+
};
|
|
68
|
+
if (svc.permission) serviceObj.$['android:permission'] = svc.permission;
|
|
69
|
+
application.service.push(serviceObj);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
65
72
|
|
|
66
73
|
return config;
|
|
67
74
|
});
|