rns-nativecall 0.1.4 → 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.
@@ -48,7 +48,15 @@ dependencies {
48
48
  // Kotlin Standard Library
49
49
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
50
50
 
51
- // Note: Telecom APIs (ConnectionService) are part of the standard Android SDK,
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
- val telecomManager =
33
- reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
34
- val phoneAccountHandle = getPhoneAccountHandle()
35
-
36
- // Dynamically get the app's display name from the Android Manifest
37
- val appName =
38
- reactApplicationContext
39
- .applicationInfo
40
- .loadLabel(reactApplicationContext.packageManager)
41
- .toString()
42
-
43
- // Correct bitmasking: Combine all capabilities into a single integer
44
- val capabilities =
45
- PhoneAccount.CAPABILITY_VIDEO_CALLING or
46
- PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING
47
-
48
- val phoneAccount =
49
- PhoneAccount.builder(phoneAccountHandle, appName)
50
- .setCapabilities(capabilities)
51
- .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
52
- .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
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
- uuid: String,
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
- val extras =
72
- Bundle().apply {
73
- putParcelable(
74
- TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
75
- Uri.fromParts("sip", number, null)
76
- )
77
- putString(TelecomManager.EXTRA_CALL_SUBJECT, name)
78
- putString("EXTRA_CALL_UUID", uuid)
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
- fun checkTelecomPermissions(promise: Promise) {
103
- val telecomManager =
104
- reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
105
- val phoneAccountHandle = getPhoneAccountHandle()
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
- val account = telecomManager.getPhoneAccount(phoneAccountHandle)
108
- if (account != null && account.isEnabled) {
109
- promise.resolve(true)
110
- } else {
111
- // Opens the system settings for "Calling Accounts" so the user can enable the app
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
- fun sendEventToJS(eventName: String, uuid: String?) {
126
- val params = Arguments.createMap().apply { putString("callUUID", uuid) }
127
- instance?.reactApplicationContext
128
- ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
129
- ?.emit(eventName, params)
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
- ///// Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/CallPackage.kt
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
- private val context: Context,
15
- private val callUUID: String?,
16
- private val playRing: Boolean,
17
- private val onAcceptCallback: (String?) -> Unit,
18
- private val onRejectCallback: (String?) -> Unit
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
- val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
34
-
35
- mediaPlayer =
36
- MediaPlayer().apply {
37
- setDataSource(context, uri)
38
- setAudioAttributes(
39
- AudioAttributes.Builder()
40
- .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
41
- .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
42
- .build()
43
- )
44
- isLooping = true
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
- // Notify JS
72
- onAcceptCallback(callUUID)
62
+ onAcceptCallback(allData)
73
63
 
74
- // 🔥 IMMEDIATELY detach from Telecom
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(callUUID)
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(callUUID)
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,7 +1,6 @@
1
- /////
2
- // Users/bush/Desktop/Apps/Raiidr/package/android/src/main/java/com/rnsnativecall/MyConnectionService.kt
3
1
  package com.rnsnativecall
4
2
 
3
+ import android.net.Uri
5
4
  import android.telecom.Connection
6
5
  import android.telecom.ConnectionRequest
7
6
  import android.telecom.ConnectionService
@@ -13,58 +12,50 @@ class MyConnectionService : ConnectionService() {
13
12
 
14
13
  companion object {
15
14
  private val activeConnections = ConcurrentHashMap<String, MyCallConnection>()
16
-
17
15
  fun getConnection(uuid: String) = activeConnections[uuid]
18
- fun addConnection(uuid: String, connection: MyCallConnection) {
19
- activeConnections[uuid] = connection
20
- }
21
- fun removeConnection(uuid: String) {
22
- activeConnections.remove(uuid)
23
- }
16
+ fun addConnection(uuid: String, connection: MyCallConnection) { activeConnections[uuid] = connection }
17
+ fun removeConnection(uuid: String) { activeConnections.remove(uuid) }
24
18
  }
25
19
 
26
20
  override fun onCreateIncomingConnection(
27
- connectionManagerPhoneAccount: PhoneAccountHandle?,
28
- request: ConnectionRequest?
21
+ connectionManagerPhoneAccount: PhoneAccountHandle?,
22
+ request: ConnectionRequest?
29
23
  ): Connection {
30
-
31
- // 1. Read extras
32
24
  val extras = request?.extras
33
25
  val callUUID = extras?.getString("EXTRA_CALL_UUID")
34
26
  val playRing = extras?.getBoolean("EXTRA_PLAY_RING", true) ?: true
35
- val callerName = extras?.getString(TelecomManager.EXTRA_CALL_SUBJECT)
36
-
37
- // 2. Create connection
38
- val connection =
39
- MyCallConnection(
40
- context = applicationContext,
41
- callUUID = callUUID,
42
- playRing = playRing,
43
- onAcceptCallback = { uuid ->
44
- CallModule.sendEventToJS("onCallAccepted", uuid)
45
- },
46
- onRejectCallback = { uuid ->
47
- CallModule.sendEventToJS("onCallRejected", uuid)
48
- }
49
- )
50
-
51
- // 3. Set caller display name
52
- callerName?.let { connection.setCallerDisplayName(it, TelecomManager.PRESENTATION_ALLOWED) }
53
-
54
- // 4. Standard Telecom setup
55
- connection.connectionCapabilities =
56
- Connection.CAPABILITY_MUTE or Connection.CAPABILITY_SUPPORT_HOLD
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
+ }
57
34
 
58
- connection.setAddress(request?.address, TelecomManager.PRESENTATION_ALLOWED)
35
+ val actualPersonName = extras?.getString("EXTRA_CALLER_NAME") ?: "Someone"
36
+ val appLabel = extras?.getString(TelecomManager.EXTRA_CALL_SUBJECT) ?: "Incoming Call"
37
+
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
+ )
46
+
47
+ connection.setCallerDisplayName(actualPersonName, TelecomManager.PRESENTATION_ALLOWED)
48
+ val labelUri = Uri.fromParts("tel", appLabel, null)
49
+ connection.setAddress(labelUri, TelecomManager.PRESENTATION_ALLOWED)
50
+
59
51
  connection.setInitializing()
60
52
  connection.setRinging()
61
53
 
62
- // ✅ EMIT EVENT TO JS (Fixed variable name to callUUID)
63
- callUUID?.let { CallModule.sendEventToJS("onCallDisplayed", it) }
64
-
65
- // 5. Track connection
66
- callUUID?.let { addConnection(it, connection) }
54
+ callUUID?.let {
55
+ addConnection(it, connection)
56
+ CallModule.sendEventToJS("onCallDisplayed", extrasMap)
57
+ }
67
58
 
68
59
  return connection
69
60
  }
70
- }
61
+ }
@@ -10,70 +10,46 @@ import android.telecom.VideoProfile
10
10
 
11
11
  object NativeCallManager {
12
12
 
13
- // Inside NativeCallManager.kt
14
-
15
- fun handleIncomingPush(context: Context, data: Map<String, String>) {
16
- val uuid = data["callId"] ?: return
17
- val handle = data["from"] ?: "Unknown"
18
- val callType = data["callType"] ?: "audio" // video or audio
19
- val name = data["name"] ?: handle
20
- val playRing = data["playRing"]?.toBoolean() ?: true
21
-
22
- // 1. Get the real App Name dynamically
23
- val appName =
24
- try {
25
- val pm = context.packageManager
26
- val ai = pm.getApplicationInfo(context.packageName, 0)
27
- pm.getApplicationLabel(ai).toString()
28
- } catch (e: Exception) {
29
- "App" // Fallback
30
- }
31
-
32
- // 2. Capitalize callType (e.g., "video" -> "Video")
33
- val capitalizedCallType =
34
- callType.replaceFirstChar {
35
- if (it.isLowerCase()) it.titlecase() else it.toString()
36
- }
37
-
38
- // 3. Construct the robust label: "Incoming [AppName] [Video/Audio] Call"
39
- val displayLabel = "Incoming $appName $capitalizedCallType Call"
40
-
41
- val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
42
- val phoneAccountHandle = getPhoneAccountHandle(context)
43
-
44
- val extras =
45
- Bundle().apply {
46
- putParcelable(
47
- TelecomManager.EXTRA_INCOMING_CALL_ADDRESS,
48
- Uri.fromParts("sip", handle, null)
49
- )
50
- putString("EXTRA_CALL_UUID", uuid)
51
- putString(
52
- TelecomManager.EXTRA_CALL_SUBJECT,
53
- displayLabel
54
- ) // Set the dynamic label
55
- putBoolean("EXTRA_PLAY_RING", playRing)
56
-
57
- // Ensure hasVideo is properly mapped to Telecom constants
58
- val isVideo = callType.equals("video", ignoreCase = true)
59
- val videoState =
60
- if (isVideo) VideoProfile.STATE_BIDIRECTIONAL
61
- else VideoProfile.STATE_AUDIO_ONLY
62
- putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState)
63
-
64
- // Pass all other data so ConnectionService can access it if needed
65
- data.forEach { (key, value) -> putString(key, value) }
66
- }
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
+ }
67
24
 
68
- try {
69
- telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
70
- } catch (e: Exception) {
71
- e.printStackTrace()
72
- }
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)
73
45
  }
74
46
 
47
+ notificationManager.notify(101, notificationBuilder.build())
48
+ }
49
+
50
+
75
51
  private fun getPhoneAccountHandle(context: Context): PhoneAccountHandle {
76
52
  val componentName = ComponentName(context, MyConnectionService::class.java)
77
53
  return PhoneAccountHandle(componentName, "${context.packageName}.voip")
78
54
  }
79
- }
55
+ }
@@ -0,0 +1,4 @@
1
+ <shape xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <solid android:color="#34C759" />
3
+ <corners android:radius="21dp" />
4
+ </shape>
@@ -0,0 +1,4 @@
1
+ <shape xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <solid android:color="#FF3B30" />
3
+ <corners android:radius="21dp" />
4
+ </shape>
@@ -0,0 +1,5 @@
1
+ <shape xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <solid android:color="#212121" />
3
+ <corners android:radius="100dp" />
4
+ <stroke android:width="1dp" android:color="#33FFFFFF" />
5
+ </shape>
@@ -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,3 @@
1
+ <style name="CircleImage">
2
+ <item name="cornerSize">50%</item>
3
+ </style>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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",
@@ -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
- if (!manifest.manifest['uses-permission']) {
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
- const application = manifest.manifest.application[0];
27
- application.service = application.service || [];
28
-
29
- // MyConnectionService
30
- const connectionServiceName = 'com.rnsnativecall.MyConnectionService';
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': connectionServiceName,
37
- 'android:permission': 'android.permission.BIND_CONNECTION_SERVICE',
38
- 'android:exported': 'true',
39
- },
40
- 'intent-filter': [
41
- {
42
- action: [{ $: { 'android:name': 'android.telecom.ConnectionService' } }]
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
- // CallMessagingService (FCM)
49
- const fcmServiceName = 'com.rnsnativecall.CallMessagingService';
50
- const existingFCMService = application.service.find((s) => s.$['android:name'] === fcmServiceName);
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
- if (!existingFCMService) {
53
- application.service.push({
54
- $: {
55
- 'android:name': fcmServiceName,
56
- 'android:exported': 'false',
57
- },
58
- 'intent-filter': [
59
- {
60
- action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }]
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
  });