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.
@@ -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
  }
@@ -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
- 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,6 +1,6 @@
1
1
  package com.rnsnativecall
2
2
 
3
- import android.net.Uri // <--- ADD THIS IMPORT
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
- activeConnections[uuid] = connection
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
- connectionManagerPhoneAccount: PhoneAccountHandle?,
27
- request: ConnectionRequest?
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
- MyCallConnection(
42
- context = applicationContext,
43
- callUUID = callUUID,
44
- playRing = playRing,
45
- onAcceptCallback = { uuid ->
46
- CallModule.sendEventToJS("onCallAccepted", uuid)
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
- // Emit event to JS so the app can send the 'ringing' status
70
- callUUID?.let { CallModule.sendEventToJS("onCallDisplayed", it) }
71
-
72
- // Track connection
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.content.ComponentName
3
+ import android.app.NotificationChannel
4
+ import android.app.NotificationManager
5
+ import android.app.PendingIntent
4
6
  import android.content.Context
5
- import android.net.Uri
6
- import android.os.Bundle
7
- import android.telecom.PhoneAccountHandle
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 handle = data["from"] ?: "Unknown"
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
- // 1. Get Dynamic App Name
21
- val appName =
22
- try {
23
- val pm = context.packageManager
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)
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
- // This goes to the PRIMARY slot (The Name) in MyConnectionService
52
- putString("EXTRA_CALLER_NAME", name)
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
- putBoolean("EXTRA_PLAY_RING", playRing)
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
- 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)
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
- // Forward all other data
63
- data.forEach { (key, value) -> putString(key, value) }
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
- try {
67
- telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
68
- } catch (e: Exception) {
69
- e.printStackTrace()
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
- // This function must be OUTSIDE handleIncomingPush but INSIDE the object
74
- private fun getPhoneAccountHandle(context: Context): PhoneAccountHandle {
75
- val componentName = ComponentName(context, MyConnectionService::class.java)
76
- return PhoneAccountHandle(componentName, "${context.packageName}.voip")
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,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,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
- const hasPerms = await ensureAndroidPermissions();
38
- 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
- }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",
@@ -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
- if (!manifest.manifest['uses-permission']) {
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
- 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);
26
+ // 2. Activities (UI Components)
27
+ application.activity = application.activity || [];
32
28
 
33
- if (!existingConnectionService) {
34
- application.service.push({
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': 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
- ]
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
- // CallMessagingService (FCM)
49
- const fcmServiceName = 'com.rnsnativecall.CallMessagingService';
50
- const existingFCMService = application.service.find((s) => s.$['android:name'] === fcmServiceName);
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': fcmServiceName,
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
- 'intent-filter': [
59
- {
60
- action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }]
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
- // 1. Add background modes
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
- withAndroidConfig,
100
- withIosConfig,
101
- ]);
102
- };
94
+ return withPlugins(config, [withAndroidConfig, withIosConfig]);
95
+ };