rns-nativecall 0.1.6 → 0.1.8

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.
@@ -0,0 +1,54 @@
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 NOTIFICATION
14
+ // Use the same ID (101) used in NativeCallManager
15
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
16
+ notificationManager.cancel(101)
17
+
18
+ // 2. EXTRACT DATA SAFELY
19
+ // We iterate through all extras and convert them to Strings for the JS Map
20
+ val dataMap = mutableMapOf<String, String>()
21
+ val extras = intent.extras
22
+ extras?.keySet()?.forEach { key ->
23
+ val value = extras.get(key)
24
+ if (value != null) {
25
+ dataMap[key] = value.toString()
26
+ }
27
+ }
28
+
29
+ // 3. EMIT EVENT TO REACT NATIVE
30
+ // This handles the case where the app is already in the background/foreground
31
+ CallModule.sendEventToJS("onCallAccepted", dataMap)
32
+
33
+ // 4. LAUNCH OR BRING MAIN APP TO FOREGROUND
34
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
35
+ if (launchIntent != null) {
36
+ launchIntent.apply {
37
+ // FLAG_ACTIVITY_SINGLE_TOP: Updates the app if it's already open
38
+ // FLAG_ACTIVITY_NEW_TASK: Required when starting from a non-activity context
39
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
40
+
41
+ // Copy all call data to the launch intent
42
+ putExtras(extras ?: Bundle())
43
+
44
+ // Helper flag for your React Native logic
45
+ putExtra("navigatingToCall", true)
46
+ }
47
+ startActivity(launchIntent)
48
+ }
49
+
50
+ // 5. FINISH TRAMPOLINE
51
+ // This ensures this invisible activity doesn't stay in the "Recent Apps" list
52
+ finish()
53
+ }
54
+ }
@@ -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,14 +1,34 @@
1
- ///
2
- // Users/bush/Desktop/Packages/rns-nativecall/android/src/main/java/com/rnsnativecall/CallMessagingService.kt
3
1
  package com.rnsnativecall
4
2
 
3
+ import android.app.ActivityManager
4
+ import android.content.Context
5
5
  import com.google.firebase.messaging.FirebaseMessagingService
6
6
  import com.google.firebase.messaging.RemoteMessage
7
7
 
8
8
  class CallMessagingService : FirebaseMessagingService() {
9
9
 
10
10
  override fun onMessageReceived(remoteMessage: RemoteMessage) {
11
- // Directly forward to your native call manager
12
- NativeCallManager.handleIncomingPush(applicationContext, remoteMessage.data)
11
+ if (isAppInForeground(applicationContext)) {
12
+ // 1. App is OPEN: Don't show the system pill.
13
+ // Just send the data to your React Native listeners.
14
+ CallModule.sendEventToJS("onCallReceived", remoteMessage.data)
15
+ } else {
16
+ // 2. App is CLOSED/BACKGROUND: Show the sticky system pill.
17
+ NativeCallManager.handleIncomingPush(applicationContext, remoteMessage.data)
18
+ }
13
19
  }
14
- }
20
+
21
+ private fun isAppInForeground(context: Context): Boolean {
22
+ val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
23
+ val appProcesses = activityManager.runningAppProcesses ?: return false
24
+ val packageName = context.packageName
25
+
26
+ for (appProcess in appProcesses) {
27
+ if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
28
+ appProcess.processName == packageName) {
29
+ return true
30
+ }
31
+ }
32
+ return false
33
+ }
34
+ }
@@ -1,5 +1,6 @@
1
1
  package com.rnsnativecall
2
2
 
3
+ import android.app.NotificationManager
3
4
  import android.content.ComponentName
4
5
  import android.content.Context
5
6
  import android.content.Intent
@@ -9,11 +10,9 @@ import android.telecom.DisconnectCause
9
10
  import android.telecom.PhoneAccount
10
11
  import android.telecom.PhoneAccountHandle
11
12
  import android.telecom.TelecomManager
13
+ import android.widget.Toast
12
14
  import com.facebook.react.bridge.*
13
15
  import com.facebook.react.modules.core.DeviceEventManagerModule
14
- import android.widget.Toast
15
- import com.google.android.material.snackbar.Snackbar
16
- import android.view.View
17
16
 
18
17
  class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
19
18
 
@@ -30,45 +29,33 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
30
29
  }
31
30
 
32
31
  private fun registerPhoneAccount() {
33
- val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
34
- val phoneAccountHandle = getPhoneAccountHandle()
35
-
36
- val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
37
-
38
- // ✅ FIXED: Removed CAPABILITY_CALL_PROVIDER
39
- // Self-managed apps only need these specific flags
40
- val capabilities = PhoneAccount.CAPABILITY_VIDEO_CALLING or
41
- PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING
42
-
43
- val phoneAccount = PhoneAccount.builder(phoneAccountHandle, appName)
44
- .setCapabilities(capabilities)
45
- .setShortDescription(appName)
46
- .addSupportedUriScheme("sip")
47
- .addSupportedUriScheme("tel")
48
- .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
49
- .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
50
- .build()
51
-
52
- telecomManager.registerPhoneAccount(phoneAccount)
53
- }
54
-
55
- @ReactMethod
56
- fun displayIncomingCall(uuid: String, number: String, name: String, hasVideo: Boolean, playRing: Boolean, promise: Promise) {
57
32
  val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
58
33
  val phoneAccountHandle = getPhoneAccountHandle()
34
+ val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
59
35
 
60
- val extras = Bundle().apply {
61
- putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, Uri.fromParts("sip", number, null))
62
- putString("EXTRA_CALL_UUID", uuid)
36
+ val capabilities = PhoneAccount.CAPABILITY_VIDEO_CALLING or
37
+ PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING or
38
+ PhoneAccount.CAPABILITY_SELF_MANAGED
63
39
 
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)
40
+ val phoneAccount = PhoneAccount.builder(phoneAccountHandle, appName)
41
+ .setCapabilities(capabilities)
42
+ .setShortDescription(appName)
43
+ .addSupportedUriScheme("sip")
44
+ .addSupportedUriScheme("tel")
45
+ .build()
68
46
 
69
- }
47
+ telecomManager.registerPhoneAccount(phoneAccount)
48
+ }
49
+
50
+ @ReactMethod
51
+ fun displayIncomingCall(uuid: String, name: String, callType: String, promise: Promise) {
70
52
  try {
71
- telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
53
+ val data = mapOf(
54
+ "callUuid" to uuid,
55
+ "name" to name,
56
+ "callType" to callType
57
+ )
58
+ NativeCallManager.handleIncomingPush(reactApplicationContext, data)
72
59
  promise.resolve(true)
73
60
  } catch (e: Exception) {
74
61
  promise.reject("CALL_ERROR", e.message)
@@ -77,68 +64,56 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
77
64
 
78
65
  @ReactMethod
79
66
  fun endNativeCall(uuid: String) {
67
+ val notificationManager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
68
+ notificationManager.cancel(101)
69
+
80
70
  val connection = MyConnectionService.getConnection(uuid)
81
71
  connection?.let {
82
- it.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
72
+ it.setDisconnected(DisconnectCause(DisconnectCause.MISSED))
83
73
  it.destroy()
84
74
  MyConnectionService.removeConnection(uuid)
85
75
  }
86
76
  }
87
77
 
88
- //done // val fallbackIntent = Intent(Intent.ACTION_MAIN).apply {
89
- // addCategory(Intent.CATEGORY_LAUNCHER)
90
- // component = ComponentName("com.android.phone", "com.android.phone.CallFeaturesSetting")
91
- // addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
92
- // }
93
- // reactApplicationContext.startActivity(fallbackIntent)
94
-
95
78
  @ReactMethod
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)
110
-
111
- toast.show()
112
-
113
- // Show again after 3.5s
114
- android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
115
- toast.show()
116
- }, 3500)
117
-
118
-
119
- // Open Calling Accounts settings
120
- val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
121
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
122
- reactApplicationContext.startActivity(intent)
79
+ fun checkTelecomPermissions(promise: Promise) {
80
+ val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
81
+ val phoneAccountHandle = getPhoneAccountHandle()
82
+ val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
123
83
 
124
- promise.resolve(false)
125
- } catch (e: Exception) {
126
- promise.reject("PERMISSION_ERROR", "Could not open settings: ${e.message}")
84
+ val account = telecomManager.getPhoneAccount(phoneAccountHandle)
85
+ if (account != null && account.isEnabled) {
86
+ promise.resolve(true)
87
+ } else {
88
+ try {
89
+ Toast.makeText(reactApplicationContext, "Tap 'Active calling accounts' and then enable $appName", Toast.LENGTH_LONG).show()
90
+ val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
91
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
92
+ reactApplicationContext.startActivity(intent)
93
+ promise.resolve(false)
94
+ } catch (e: Exception) {
95
+ promise.reject("PERMISSION_ERROR", e.message)
96
+ }
127
97
  }
128
98
  }
129
- }
130
99
 
100
+ @ReactMethod
101
+ fun getInitialCallData(promise: Promise) {
102
+ promise.resolve(pendingCallData)
103
+ pendingCallData = null // Clear after consumption
104
+ }
131
105
 
132
106
  @ReactMethod fun addListener(eventName: String) {}
133
107
  @ReactMethod fun removeListeners(count: Int) {}
134
108
 
135
109
  companion object {
136
110
  private var instance: CallModule? = null
111
+ private var pendingCallData: WritableMap? = null
137
112
 
138
113
  @JvmStatic
139
114
  fun sendEventToJS(eventName: String, params: Any?) {
140
- val reactContext = instance?.reactApplicationContext ?: return
141
-
115
+ val reactContext = instance?.reactApplicationContext
116
+
142
117
  val bridgeData = when (params) {
143
118
  is Map<*, *> -> {
144
119
  val map = Arguments.createMap()
@@ -148,14 +123,21 @@ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
148
123
  map
149
124
  }
150
125
  is String -> {
151
- Arguments.createMap().apply { putString("callUUID", params) }
126
+ Arguments.createMap().apply { putString("callUuid", params) }
152
127
  }
153
128
  else -> null
154
129
  }
155
130
 
156
- reactContext
157
- .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
158
- ?.emit(eventName, bridgeData)
131
+ if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
132
+ reactContext
133
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
134
+ ?.emit(eventName, bridgeData)
135
+ } else {
136
+ // If app is dead/cold-starting, cache the accept event
137
+ if (eventName == "onCallAccepted") {
138
+ pendingCallData = bridgeData
139
+ }
140
+ }
159
141
  }
160
142
  }
161
143
  }
@@ -1,55 +1,95 @@
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
- // 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
- }
13
+ fun handleIncomingPush(context: Context, data: Map<String, String>) {
14
+ val uuid = data["callUuid"] ?: return
15
+ val name = data["name"] ?: "Incoming Call"
16
+ val callType = data["callType"] ?: "audio"
24
17
 
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)
45
- }
18
+ // Use MUTABLE flag to ensure extras are properly passed on Android 12+
19
+ val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
20
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
21
+ } else {
22
+ PendingIntent.FLAG_UPDATE_CURRENT
23
+ }
24
+
25
+ // 1. DUMMY INTENT: Keeps the notification "Sticky" (Persistent)
26
+ val dummyIntent = PendingIntent.getActivity(
27
+ context,
28
+ 0,
29
+ Intent(),
30
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
31
+ )
32
+
33
+ // 2. Accept Action - Use unique action and requestCode
34
+ val acceptIntent = Intent(context, AcceptCallActivity::class.java).apply {
35
+ // Unique action string prevents intent caching issues
36
+ action = "ACTION_ACCEPT_$uuid"
37
+ putExtra("EXTRA_CALL_UUID", uuid)
38
+ // Flatten the map into the intent extras
39
+ data.forEach { (key, value) -> putExtra(key, value) }
40
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
41
+ }
42
+
43
+ val acceptPendingIntent = PendingIntent.getActivity(
44
+ context,
45
+ uuid.hashCode(), // Unique RequestCode
46
+ acceptIntent,
47
+ pendingFlags
48
+ )
49
+
50
+ // 3. Reject Action - Use unique action and requestCode
51
+ val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
52
+ action = "ACTION_REJECT_$uuid"
53
+ putExtra("EXTRA_CALL_UUID", uuid)
54
+ }
55
+ val rejectPendingIntent = PendingIntent.getBroadcast(
56
+ context,
57
+ uuid.hashCode() + 1, // Unique RequestCode
58
+ rejectIntent,
59
+ pendingFlags
60
+ )
61
+
62
+ // 4. Setup Channel
63
+ val channelId = "CALL_CHANNEL_ID"
64
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
65
+
66
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
67
+ val channel = NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
68
+ description = "Shows incoming call notifications"
69
+ enableVibration(true)
70
+ setBypassDnd(true)
71
+ lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
72
+ }
73
+ notificationManager.createNotificationChannel(channel)
74
+ }
46
75
 
47
- notificationManager.notify(101, notificationBuilder.build())
48
- }
49
-
76
+ // 5. Build the Notification
77
+ val builder = NotificationCompat.Builder(context, channelId)
78
+ .setSmallIcon(context.applicationInfo.icon)
79
+ .setContentTitle("Incoming $callType call")
80
+ .setContentText(name)
81
+ .setPriority(NotificationCompat.PRIORITY_MAX)
82
+ .setCategory(NotificationCompat.CATEGORY_CALL)
83
+ .setOngoing(true)
84
+ .setAutoCancel(false)
85
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
86
+
87
+ // This hack keeps the Heads-Up "Pill" visible until action is taken
88
+ .setFullScreenIntent(dummyIntent, true)
89
+
90
+ .addAction(0, "Answer", acceptPendingIntent)
91
+ .addAction(0, "Decline", rejectPendingIntent)
50
92
 
51
- private fun getPhoneAccountHandle(context: Context): PhoneAccountHandle {
52
- val componentName = ComponentName(context, MyConnectionService::class.java)
53
- return PhoneAccountHandle(componentName, "${context.packageName}.voip")
93
+ notificationManager.notify(101, builder.build())
54
94
  }
55
95
  }
@@ -1,3 +1,12 @@
1
- <style name="CircleImage">
2
- <item name="cornerSize">50%</item>
3
- </style>
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.d.ts CHANGED
@@ -18,18 +18,14 @@ export interface CallHandlerType {
18
18
  /**
19
19
  * Display an incoming call UI using ConnectionService (Android) or CallKit (iOS).
20
20
  * @param uuid Unique call identifier
21
- * @param number Caller number (or URI)
22
21
  * @param name Caller display name
23
- * @param hasVideo True if video call (default: false)
24
- * @param shouldRing True to play native ringtone (default: true)
22
+ * @param callType True if video call (default: false)
25
23
  * @returns Promise resolving to true if successfully displayed
26
24
  */
27
25
  displayCall(
28
26
  uuid: string,
29
- number: string,
30
27
  name: string,
31
- hasVideo?: boolean,
32
- shouldRing?: boolean
28
+ callType: string,
33
29
  ): Promise<boolean>;
34
30
 
35
31
  /**
package/index.js CHANGED
@@ -35,21 +35,19 @@ export async function ensureAndroidPermissions() {
35
35
  }
36
36
 
37
37
  export const CallHandler = {
38
- displayCall: async (uuid, number, name, hasVideo = false, shouldRing = true) => {
38
+ displayCall: async (uuid, name, callType) => {
39
39
  if (!CallModule) return false;
40
40
 
41
- if (Platform.OS === 'android') {
42
- const hasPerms = await ensureAndroidPermissions();
43
- if (!hasPerms) return false;
44
- }
41
+ // if (Platform.OS === 'android') {
42
+ // const hasPerms = await ensureAndroidPermissions();
43
+ // if (!hasPerms) return false;
44
+ // }
45
45
 
46
46
  try {
47
47
  return await CallModule.displayIncomingCall(
48
48
  uuid.toLowerCase().trim(),
49
- number,
50
49
  name,
51
- hasVideo,
52
- shouldRing
50
+ callType,
53
51
  );
54
52
  } catch (e) {
55
53
  // console.log("Native Call Error:", e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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,14 +1,41 @@
1
- const { withAndroidManifest, withInfoPlist, withPlugins } = require('@expo/config-plugins');
1
+ const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity } = require('@expo/config-plugins');
2
2
 
3
- /**
4
- * ANDROID CONFIGURATION
5
- */
3
+ /** 1. ANDROID MAIN ACTIVITY MOD (Fixes data passing) **/
4
+ function withMainActivityDataFix(config) {
5
+ return withMainActivity(config, (config) => {
6
+ let contents = config.modResults.contents;
7
+
8
+ // Add necessary import
9
+ if (!contents.includes('import android.content.Intent')) {
10
+ contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
11
+ }
12
+
13
+ // Add onNewIntent override to ensure data is updated when app is already open
14
+ const onNewIntentCode = `
15
+ override fun onNewIntent(intent: Intent) {
16
+ super.onNewIntent(intent)
17
+ setIntent(intent)
18
+ }
19
+ `;
20
+
21
+ if (!contents.includes('override fun onNewIntent')) {
22
+ // Find the last closing brace and insert before it
23
+ const lastBraceIndex = contents.lastIndexOf('}');
24
+ contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
25
+ }
26
+
27
+ config.modResults.contents = contents;
28
+ return config;
29
+ });
30
+ }
31
+
32
+ /** 2. ANDROID MANIFEST CONFIG **/
6
33
  function withAndroidConfig(config) {
7
34
  return withAndroidManifest(config, (config) => {
8
35
  const manifest = config.modResults;
9
36
  const application = manifest.manifest.application[0];
10
37
 
11
- // 1. Unified Permissions List
38
+ // Permissions
12
39
  const permissions = [
13
40
  'android.permission.READ_PHONE_NUMBERS',
14
41
  'android.permission.CALL_PHONE',
@@ -26,84 +53,67 @@ function withAndroidConfig(config) {
26
53
  }
27
54
  });
28
55
 
29
- // 2. Register IncomingCallActivity
56
+ // Activities
30
57
  application.activity = application.activity || [];
31
- const activityName = 'com.rnsnativecall.IncomingCallActivity';
32
- if (!application.activity.some(a => a.$['android:name'] === activityName)) {
58
+
59
+ // Update MainActivity to use singleTask for better call handling
60
+ const mainActivity = application.activity.find(a => a.$['android:name'] === '.MainActivity');
61
+ if (mainActivity) {
62
+ mainActivity.$['android:launchMode'] = 'singleTask';
63
+ }
64
+
65
+ // AcceptCallActivity (Trampoline)
66
+ if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
33
67
  application.activity.push({
34
68
  $: {
35
- 'android:name': activityName,
36
- 'android:showOnLockScreen': 'true',
37
- 'android:launchMode': 'singleInstance',
69
+ 'android:name': 'com.rnsnativecall.AcceptCallActivity',
70
+ 'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
38
71
  'android:excludeFromRecents': 'true',
39
- 'android:screenOrientation': 'portrait',
40
- 'android:theme': '@style/Theme.AppCompat.Light.NoActionBar'
72
+ 'android:noHistory': 'true',
73
+ 'android:exported': 'false',
74
+ 'android:launchMode': 'singleInstance'
41
75
  }
42
76
  });
43
77
  }
44
78
 
45
- // 3. Services (ConnectionService & FCM)
79
+ // Services
46
80
  application.service = application.service || [];
47
-
48
81
  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
- }
82
+ { name: 'com.rnsnativecall.MyConnectionService', permission: 'android.permission.BIND_CONNECTION_SERVICE', action: 'android.telecom.ConnectionService' },
83
+ { name: 'com.rnsnativecall.CallMessagingService', action: 'com.google.firebase.MESSAGING_EVENT' }
60
84
  ];
61
85
 
62
86
  services.forEach(svc => {
63
87
  if (!application.service.some(s => s.$['android:name'] === svc.name)) {
64
- const serviceObj = {
65
- $: { 'android:name': svc.name, 'android:exported': svc.exported },
88
+ application.service.push({
89
+ $: { 'android:name': svc.name, 'android:exported': svc.permission ? 'true' : 'false', 'android:permission': svc.permission },
66
90
  'intent-filter': [{ action: [{ $: { 'android:name': svc.action } }] }]
67
- };
68
- if (svc.permission) serviceObj.$['android:permission'] = svc.permission;
69
- application.service.push(serviceObj);
91
+ });
70
92
  }
71
93
  });
72
94
 
95
+ // Receivers
96
+ application.receiver = application.receiver || [];
97
+ if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
98
+ application.receiver.push({
99
+ $: { 'android:name': 'com.rnsnativecall.CallActionReceiver', 'android:exported': 'false' }
100
+ });
101
+ }
102
+
73
103
  return config;
74
104
  });
75
105
  }
76
106
 
77
- /**
78
- * IOS CONFIGURATION
79
- */
107
+ /** 3. IOS CONFIG **/
80
108
  function withIosConfig(config) {
81
109
  return withInfoPlist(config, (config) => {
82
110
  const infoPlist = config.modResults;
83
-
84
- // 1. Add background modes
85
- if (!infoPlist.UIBackgroundModes) {
86
- infoPlist.UIBackgroundModes = [];
87
- }
88
-
89
- ['voip', 'audio'].forEach((mode) => {
90
- if (!infoPlist.UIBackgroundModes.includes(mode)) {
91
- infoPlist.UIBackgroundModes.push(mode);
92
- }
93
- });
94
-
95
- // 2. Dynamic Microphone Description
96
- const appName = config.name || 'this app';
97
- infoPlist.NSMicrophoneUsageDescription =
98
- infoPlist.NSMicrophoneUsageDescription || `Allow ${appName} to access your microphone for calls.`;
99
-
111
+ if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
112
+ ['voip', 'audio'].forEach(mode => { if (!infoPlist.UIBackgroundModes.includes(mode)) infoPlist.UIBackgroundModes.push(mode); });
100
113
  return config;
101
114
  });
102
115
  }
103
116
 
104
117
  module.exports = (config) => {
105
- return withPlugins(config, [
106
- withAndroidConfig,
107
- withIosConfig,
108
- ]);
109
- };
118
+ return withPlugins(config, [withAndroidConfig, withMainActivityDataFix, withIosConfig]);
119
+ };