rns-nativecall 0.3.5 → 0.3.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.
@@ -9,16 +9,10 @@ import android.os.Bundle
9
9
  class AcceptCallActivity : Activity() {
10
10
  override fun onCreate(savedInstanceState: Bundle?) {
11
11
  super.onCreate(savedInstanceState)
12
- NativeCallManager.stopRingtone()
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>()
12
+
13
+ // 1. EXTRACT DATA SAFELY
21
14
  val extras = intent.extras
15
+ val dataMap = mutableMapOf<String, String>()
22
16
  extras?.keySet()?.forEach { key ->
23
17
  val value = extras.get(key)
24
18
  if (value != null) {
@@ -26,30 +20,38 @@ class AcceptCallActivity : Activity() {
26
20
  }
27
21
  }
28
22
 
23
+ val uuid = dataMap["callUuid"]
24
+
25
+ // 2. STOP RINGING & CLEAR THE SPECIFIC NOTIFICATION
26
+ NativeCallManager.stopRingtone()
27
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
28
+
29
+ if (uuid != null) {
30
+ // MATCHING THE NEW HASHCODE LOGIC
31
+ notificationManager.cancel(uuid.hashCode())
32
+ } else {
33
+ // Fallback for safety
34
+ notificationManager.cancel(101)
35
+ }
36
+
29
37
  // 3. EMIT EVENT TO REACT NATIVE
30
- // This handles the case where the app is already in the background/foreground
31
38
  CallModule.sendEventToJS("onCallAccepted", dataMap)
39
+
40
+ // Ensure the data is available for the JS bridge even if it's just waking up
41
+ CallModule.setPendingCallData(dataMap)
32
42
 
33
43
  // 4. LAUNCH OR BRING MAIN APP TO FOREGROUND
34
44
  val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
35
45
  if (launchIntent != null) {
36
46
  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
47
  addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
40
-
41
- // Copy all call data to the launch intent
42
48
  putExtras(extras ?: Bundle())
43
-
44
- // Helper flag for your React Native logic
45
49
  putExtra("navigatingToCall", true)
46
50
  }
47
51
  startActivity(launchIntent)
48
52
  }
49
53
 
50
- CallModule.setPendingCallData(dataMap)
51
54
  // 5. FINISH TRAMPOLINE
52
- // This ensures this invisible activity doesn't stay in the "Recent Apps" list
53
55
  finish()
54
56
  }
55
57
  }
@@ -8,30 +8,36 @@ import android.os.Bundle
8
8
 
9
9
  class CallActionReceiver : BroadcastReceiver() {
10
10
  override fun onReceive(context: Context, intent: Intent) {
11
+ // 1. Always stop the ringtone immediately
11
12
  NativeCallManager.stopRingtone()
13
+
12
14
  val uuid = intent.getStringExtra("EXTRA_CALL_UUID")
13
15
 
14
- // 1. Clear the notification
16
+ val action = intent.action
17
+
18
+ // 2. Clear the specific notification using the hash code
15
19
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
16
- notificationManager.cancel(101)
20
+ if (uuid != null) {
21
+ notificationManager.cancel(uuid.hashCode())
22
+ } else {
23
+ notificationManager.cancel(101) // Fallback for safety
24
+ }
17
25
 
18
- if (intent.action == "ACTION_ACCEPT") {
19
- // 2. Prepare the data for React Native
26
+ // 3. Handle Actions
27
+ // We check for .contains or .startsWith because our actions are "ACTION_REJECT_$uuid"
28
+ if (action?.contains("ACTION_ACCEPT") == true || action?.contains("ACTION_SHOW_UI") == true) {
20
29
  val dataMap = mutableMapOf<String, String>()
21
30
  intent.extras?.keySet()?.forEach { key ->
22
- // Using get(key).toString() is safer than getString(key)
23
- // because some extras might be Ints or Booleans
24
31
  intent.extras?.get(key)?.let { dataMap[key] = it.toString() }
25
32
  }
26
33
 
27
- // --- THE COLD START FIX ---
28
- // Store data in the "Holding Gate" inside CallModule
34
+ // Store data for the JS Bridge "Holding Gate"
29
35
  CallModule.setPendingCallData(dataMap)
30
36
 
31
- // 3. Send event (works if app is already active)
37
+ // Emit event for active JS bridge
32
38
  CallModule.sendEventToJS("onCallAccepted", dataMap)
33
39
 
34
- // 4. Bring the app to foreground
40
+ // Bring app to foreground
35
41
  val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
36
42
  launchIntent?.apply {
37
43
  addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
@@ -40,7 +46,7 @@ class CallActionReceiver : BroadcastReceiver() {
40
46
  }
41
47
  context.startActivity(launchIntent)
42
48
 
43
- } else if (intent.action == "ACTION_REJECT") {
49
+ } else if (action?.contains("ACTION_REJECT") == true) {
44
50
  // Logic for Reject
45
51
  CallModule.sendEventToJS("onCallRejected", mapOf("callUuid" to (uuid ?: "")))
46
52
  }
@@ -6,15 +6,15 @@ import com.facebook.react.bridge.Arguments
6
6
  import com.facebook.react.jstasks.HeadlessJsTaskConfig
7
7
 
8
8
  class CallHeadlessTask : HeadlessJsTaskService() {
9
- override fun getTaskConfig(intent: Intent): HeadlessJsTaskConfig? {
10
- val extras = intent.extras
9
+ // Note: The '?' after Intent and the return type are specific in Kotlin
10
+ override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
11
+ val extras = intent?.extras
11
12
  return if (extras != null) {
12
- // INCREASE timeout to 30000 (30s) so it covers your 18s delay
13
13
  HeadlessJsTaskConfig(
14
14
  "ColdStartCallTask",
15
15
  Arguments.fromBundle(extras),
16
16
  30000,
17
- true // Allow in foreground
17
+ true
18
18
  )
19
19
  } else {
20
20
  null
@@ -7,6 +7,9 @@ import android.os.Bundle
7
7
  import android.os.Handler
8
8
  import android.os.Looper
9
9
  import android.app.NotificationManager
10
+ import android.app.NotificationChannel
11
+ import android.app.PendingIntent
12
+ import android.os.Build
10
13
  import androidx.core.app.NotificationCompat
11
14
  import com.google.firebase.messaging.FirebaseMessagingService
12
15
  import com.google.firebase.messaging.RemoteMessage
@@ -14,7 +17,6 @@ import com.facebook.react.HeadlessJsTaskService
14
17
 
15
18
  class CallMessagingService : FirebaseMessagingService() {
16
19
 
17
- // This companion object allows the Cancel logic to find the active timer
18
20
  companion object {
19
21
  private val handler = Handler(Looper.getMainLooper())
20
22
  private val pendingNotifications = mutableMapOf<String, Runnable>()
@@ -23,96 +25,99 @@ class CallMessagingService : FirebaseMessagingService() {
23
25
  override fun onMessageReceived(remoteMessage: RemoteMessage) {
24
26
  val data = remoteMessage.data
25
27
  val context = applicationContext
26
-
27
- // Extract variables for logic
28
28
  val uuid = data["callUuid"] ?: return
29
29
  val type = data["type"] ?: ""
30
30
 
31
- // CASE 1: CALLER HUNG UP / CANCELLED
32
31
  if (type == "CANCEL") {
33
- // 1. Remove the pending 18s timer for this specific call
34
- pendingNotifications[uuid]?.let {
35
- handler.removeCallbacks(it)
36
- pendingNotifications.remove(uuid)
37
- }
38
-
39
- // 2. Stop the ringtone/Pill if it had already started showing
40
- NativeCallManager.stopRingtone()
41
- CallHandler.destroyNativeCallUI(uuid) // Ensure native UI is killed
42
-
43
- // 3. Show standard "Missed Call" notification
44
- showMissedCallNotification(context, data, uuid)
45
- return
46
- }
32
+ pendingNotifications[uuid]?.let {
33
+ handler.removeCallbacks(it)
34
+ pendingNotifications.remove(uuid)
35
+ }
36
+
37
+ // Pass the uuid so the manager knows exactly which notification to remove
38
+ NativeCallManager.dismissIncomingCall(context, uuid)
39
+
40
+ showMissedCallNotification(context, data, uuid)
41
+ return
42
+ }
47
43
 
48
- // CASE 2: NEW INCOMING CALL
49
44
  if (isAppInForeground(context)) {
50
- // App is visible: Send event to active JS bridge immediately
51
45
  CallModule.sendEventToJS("onCallReceived", data)
52
46
  } else {
53
- // 1. Wake the actual App process by launching the Intent
54
- val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
55
- launchIntent?.apply {
56
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
57
- // This is key: it tells the app to boot the JS but stay minimized
58
- putExtra("background_wake", true)
59
- }
60
- context.startActivity(launchIntent)
47
+ // 1. Prepare the Intent
48
+ val headlessIntent = Intent(context, CallHeadlessTask::class.java)
49
+ val bundle = Bundle()
50
+ data.forEach { (k, v) -> bundle.putString(k, v) }
51
+ headlessIntent.putExtras(bundle)
61
52
 
62
- // You SHOULD see "Android Bundled..." now because the Activity is starting
53
+ // 2. Start the Service
54
+ context.startService(headlessIntent)
63
55
 
56
+ // 3. Acquire WakeLock to prevent the CPU from sleeping during bundling
57
+ try {
58
+ HeadlessJsTaskService.acquireWakeLockNow(context)
59
+ } catch (e: Exception) {
60
+ e.printStackTrace()
61
+ }
62
+
63
+ // 4. Start the 18s backup timer
64
64
  val showNotificationRunnable = Runnable {
65
65
  if (!isAppInForeground(context)) {
66
66
  NativeCallManager.handleIncomingPush(context, data)
67
67
  }
68
+ pendingNotifications.remove(uuid)
68
69
  }
69
- handler.postDelayed(showNotificationRunnable, 18000)
70
+ pendingNotifications[uuid] = showNotificationRunnable
71
+ handler.postDelayed(showNotificationRunnable, 18000)
70
72
  }
71
73
  }
72
74
 
73
- private fun startHeadlessTask(context: Context, data: Map<String, String>) {
74
- val serviceIntent = Intent(context, CallHeadlessTask::class.java)
75
- val bundle = Bundle()
76
- data.forEach { (key, value) -> bundle.putString(key, value) }
77
- serviceIntent.putExtras(bundle)
78
-
79
- context.startService(serviceIntent)
80
- HeadlessJsTaskService.acquireWakeLockNow(context)
81
- }
82
-
83
75
  private fun showMissedCallNotification(context: Context, data: Map<String, String>, uuid: String) {
84
76
  val name = data["name"] ?: "Unknown"
85
77
  val callType = data["callType"] ?: "video"
86
- val channelId = "missed_calls" // Ensure this channel is created in your MainApplication or CallManager
78
+ val channelId = "missed_calls"
79
+ val article = if (callType.startsWith("a", ignoreCase = true)) "an" else "a"
80
+ // 1. Format App Name: Capitalized
81
+ val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
82
+ val capitalizedAppName = appName.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
87
83
 
88
84
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
89
85
 
90
- // Use ic_launcher as fallback if ic_missed_call isn't found
86
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
87
+ val channel = NotificationChannel(channelId, "Missed Calls", NotificationManager.IMPORTANCE_HIGH)
88
+ notificationManager.createNotificationChannel(channel)
89
+ }
90
+
91
+ // 2. Create Intent to open app when clicking missed call
92
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
93
+ val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
94
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
95
+ } else {
96
+ PendingIntent.FLAG_UPDATE_CURRENT
97
+ }
98
+ val contentIntent = PendingIntent.getActivity(context, uuid.hashCode(), launchIntent, pendingFlags)
99
+
91
100
  var iconResId = context.resources.getIdentifier("ic_missed_call", "drawable", context.packageName)
92
101
  if (iconResId == 0) iconResId = android.R.drawable.sym_call_missed
93
102
 
94
103
  val builder = NotificationCompat.Builder(context, channelId)
95
104
  .setSmallIcon(iconResId)
96
- .setContentTitle("Missed $callType call")
97
- .setContentText("You missed a call from $name")
105
+ .setContentTitle("$capitalizedAppName missed call")
106
+ .setContentText("You missed $article $callType call from $name")
98
107
  .setPriority(NotificationCompat.PRIORITY_HIGH)
99
108
  .setAutoCancel(true)
100
- .setCategory(NotificationCompat.CATEGORY_MISS)
109
+ .setContentIntent(contentIntent) // Opens app on click
110
+ .setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
101
111
 
112
+ // 3. Use unique hashCode so multiple missed calls are stacked separately
102
113
  notificationManager.notify(uuid.hashCode(), builder.build())
103
114
  }
104
115
 
105
116
  private fun isAppInForeground(context: Context): Boolean {
106
117
  val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
107
118
  val appProcesses = activityManager.runningAppProcesses ?: return false
108
- val packageName = context.packageName
109
-
110
- for (appProcess in appProcesses) {
111
- if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
112
- appProcess.processName == packageName) {
113
- return true
114
- }
119
+ return appProcesses.any {
120
+ it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && it.processName == context.packageName
115
121
  }
116
- return false
117
122
  }
118
123
  }
@@ -14,7 +14,8 @@ import android.graphics.Color
14
14
  object NativeCallManager {
15
15
 
16
16
  private var ringtone: Ringtone? = null
17
- private const val NOTIFICATION_ID = 101
17
+ const val INCOMING_CALL_ID = 101
18
+ const val CALL_CHANNEL_ID = "CALL_CHANNEL_ID"
18
19
 
19
20
  fun handleIncomingPush(context: Context, data: Map<String, String>) {
20
21
  val uuid = data["callUuid"] ?: return
@@ -28,56 +29,36 @@ object NativeCallManager {
28
29
  } else {
29
30
  PendingIntent.FLAG_UPDATE_CURRENT
30
31
  }
31
- // 1. IMPROVED INTENT: Direct to AcceptCallActivity
32
- // This is better than a dummy intent because it allows the OS to
33
- // launch your "Accept" logic immediately if the phone is locked.
32
+
34
33
  val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
35
34
  action = "ACTION_SHOW_UI_$uuid"
36
35
  data.forEach { (key, value) -> putExtra(key, value) }
37
36
  addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
38
37
  }
39
38
 
40
- val fullScreenPendingIntent = PendingIntent.getActivity(
41
- context,
42
- uuid.hashCode(),
43
- intentToActivity,
44
- pendingFlags
45
- )
39
+ val fullScreenPendingIntent = PendingIntent.getActivity(context, uuid.hashCode(), intentToActivity, pendingFlags)
46
40
 
47
- // 2. Reject Action - Still goes to BroadcastReceiver
48
41
  val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
49
42
  action = "ACTION_REJECT_$uuid"
50
43
  putExtra("EXTRA_CALL_UUID", uuid)
51
- // Pass all data so onReject can find the UUID
52
44
  data.forEach { (key, value) -> putExtra(key, value) }
53
45
  }
54
- val rejectPendingIntent = PendingIntent.getBroadcast(
55
- context,
56
- uuid.hashCode() + 1,
57
- rejectIntent,
58
- pendingFlags
59
- )
60
-
61
- // 3. Setup Channel with high priority
62
- val channelId = "CALL_CHANNEL_ID"
46
+
47
+
48
+ val rejectPendingIntent = PendingIntent.getBroadcast(context, uuid.hashCode() + 1, rejectIntent, pendingFlags)
49
+
63
50
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
64
51
 
65
52
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
66
- val channel = NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
67
- description = "Shows incoming call notifications"
68
- enableVibration(true)
69
- vibrationPattern = longArrayOf(0, 500, 500, 500)
70
- lightColor = Color.GREEN
53
+ val channel = NotificationChannel(CALL_CHANNEL_ID, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
71
54
  setBypassDnd(true)
72
55
  lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
73
- // On Android O+, sound should be set on the channel
74
56
  setSound(null, null)
75
57
  }
76
58
  notificationManager.createNotificationChannel(channel)
77
59
  }
78
60
 
79
- // 4. Build the Notification
80
- val builder = NotificationCompat.Builder(context, channelId)
61
+ val builder = NotificationCompat.Builder(context, CALL_CHANNEL_ID)
81
62
  .setSmallIcon(context.applicationInfo.icon)
82
63
  .setContentTitle("Incoming $callType call")
83
64
  .setContentText(name)
@@ -85,33 +66,47 @@ object NativeCallManager {
85
66
  .setCategory(NotificationCompat.CATEGORY_CALL)
86
67
  .setOngoing(true)
87
68
  .setAutoCancel(false)
88
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
89
-
90
- // This is the key for the "Pill" / Heads-up display
91
69
  .setFullScreenIntent(fullScreenPendingIntent, true)
92
-
93
- .addAction(0, "Answer", fullScreenPendingIntent) // Same intent as fullScreen
70
+ .addAction(0, "Answer", fullScreenPendingIntent)
94
71
  .addAction(0, "Decline", rejectPendingIntent)
95
72
 
96
- notificationManager.notify(NOTIFICATION_ID, builder.build())
73
+ notificationManager.notify(uuid.hashCode(), builder.build())
97
74
 
98
- // 5. Ringtone with looping logic attempt
99
75
  try {
100
76
  val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
101
77
  ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
102
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
103
- ringtone?.isLooping = true // Android 9+ supports looping natively
104
- }
78
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) ringtone?.isLooping = true
105
79
  ringtone?.play()
106
- } catch (e: Exception) {
107
- e.printStackTrace()
108
- }
80
+ } catch (e: Exception) { e.printStackTrace() }
109
81
  }
110
82
 
111
83
  fun stopRingtone() {
112
- if (ringtone?.isPlaying == true) {
113
- ringtone?.stop()
84
+ try {
85
+ // Use the safe call operator ?. to only run if ringtone isn't null
86
+ ringtone?.let {
87
+ if (it.isPlaying) {
88
+ it.stop()
89
+ }
114
90
  }
91
+ // Always null it out after stopping
92
+ ringtone = null
93
+ } catch (e: Exception) {
94
+ // Prevent the app from crashing if the system Ringtone service is acting up
95
+ e.printStackTrace()
115
96
  ringtone = null
116
97
  }
98
+ }
99
+
100
+ fun dismissIncomingCall(context: Context, uuid: String?) {
101
+ stopRingtone()
102
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
103
+
104
+ if (uuid != null) {
105
+ notificationManager.cancel(uuid.hashCode())
106
+ } else {
107
+ // Fallback: If for some reason uuid is null, cancel everything
108
+ // (Optional: only if you want a safety net)
109
+ // notificationManager.cancelAll()
110
+ }
111
+ }
117
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.3.5",
3
+ "version": "0.3.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",
@@ -43,6 +43,7 @@
43
43
  "react-native.config.js",
44
44
  "README.md",
45
45
  "rns-nativecall.podspec",
46
+ "withCallNativeConfig.js",
46
47
  "withNativeCallVoip.js"
47
48
  ],
48
49
  "peerDependencies": {
@@ -0,0 +1,147 @@
1
+ const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity } = require('@expo/config-plugins');
2
+
3
+ /** 1. ANDROID MAIN ACTIVITY MODS **/
4
+ function withMainActivityDataFix(config) {
5
+ return withMainActivity(config, (config) => {
6
+ let contents = config.modResults.contents;
7
+
8
+ // Ensure necessary Imports
9
+ if (!contents.includes('import android.content.Intent')) {
10
+ contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
11
+ }
12
+ if (!contents.includes('import android.os.Bundle')) {
13
+ contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.os.Bundle`);
14
+ }
15
+
16
+ const onNewIntentCode = `
17
+ override fun onNewIntent(intent: Intent) {
18
+ super.onNewIntent(intent)
19
+ setIntent(intent)
20
+ }
21
+ `;
22
+
23
+ const onCreateCode = `
24
+ override fun onCreate(savedInstanceState: Bundle?) {
25
+ super.onCreate(savedInstanceState)
26
+ // If woken up by a background task, push to back immediately
27
+ if (intent.getBooleanExtra("background_wake", false)) {
28
+ moveTaskToBack(true)
29
+ }
30
+ }
31
+ `;
32
+
33
+ // Insertion logic
34
+ if (!contents.includes('override fun onNewIntent')) {
35
+ contents = contents.replace(/class MainActivity : .*/, (match) => `${match}\n${onNewIntentCode}`);
36
+ }
37
+
38
+ if (!contents.includes('override fun onCreate')) {
39
+ // Find the end of the class or after onNewIntent
40
+ const lastBraceIndex = contents.lastIndexOf('}');
41
+ contents = contents.slice(0, lastBraceIndex) + onCreateCode + contents.slice(lastBraceIndex);
42
+ }
43
+
44
+ config.modResults.contents = contents;
45
+ return config;
46
+ });
47
+ }
48
+
49
+ /** 2. ANDROID MANIFEST CONFIG **/
50
+ function withAndroidConfig(config) {
51
+ return withAndroidManifest(config, (config) => {
52
+ const manifest = config.modResults.manifest;
53
+ const application = manifest.application[0];
54
+
55
+ // 1. Permissions (Updated for Android 14 compatibility)
56
+ const permissions = [
57
+ 'android.permission.USE_FULL_SCREEN_INTENT',
58
+ 'android.permission.VIBRATE',
59
+ 'android.permission.FOREGROUND_SERVICE',
60
+ 'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
61
+ 'android.permission.POST_NOTIFICATIONS',
62
+ 'android.permission.WAKE_LOCK',
63
+ 'android.permission.DISABLE_KEYGUARD',
64
+ 'android.permission.RECEIVE_BOOT_COMPLETED'
65
+ ];
66
+
67
+ manifest['uses-permission'] = manifest['uses-permission'] || [];
68
+ permissions.forEach((perm) => {
69
+ if (!manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
70
+ manifest['uses-permission'].push({ $: { 'android:name': perm } });
71
+ }
72
+ });
73
+
74
+ // 2. Activity Setup (AcceptCallActivity)
75
+ application.activity = application.activity || [];
76
+ if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
77
+ application.activity.push({
78
+ $: {
79
+ 'android:name': 'com.rnsnativecall.AcceptCallActivity',
80
+ 'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
81
+ 'android:excludeFromRecents': 'true',
82
+ 'android:noHistory': 'true',
83
+ 'android:exported': 'false',
84
+ 'android:launchMode': 'singleInstance',
85
+ 'android:showWhenLocked': 'true',
86
+ 'android:turnScreenOn': 'true'
87
+ }
88
+ });
89
+ }
90
+
91
+ // 3. Service Registration (The Headless Enforcer)
92
+ application.service = application.service || [];
93
+
94
+ // Firebase Messaging Service
95
+ if (!application.service.some(s => s.$['android:name'] === 'com.rnsnativecall.CallMessagingService')) {
96
+ application.service.push({
97
+ $: {
98
+ 'android:name': 'com.rnsnativecall.CallMessagingService',
99
+ 'android:exported': 'false'
100
+ },
101
+ 'intent-filter': [{
102
+ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }]
103
+ }]
104
+ });
105
+ }
106
+
107
+ // Headless Task Service
108
+ if (!application.service.some(s => s.$['android:name'] === 'com.rnsnativecall.CallHeadlessTask')) {
109
+ application.service.push({
110
+ $: {
111
+ 'android:name': 'com.rnsnativecall.CallHeadlessTask',
112
+ 'android:exported': 'false' // Headless JS should be internal only
113
+ }
114
+ });
115
+ }
116
+
117
+ // 4. Action Receiver
118
+ application.receiver = application.receiver || [];
119
+ if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
120
+ application.receiver.push({
121
+ $: { 'android:name': 'com.rnsnativecall.CallActionReceiver', 'android:exported': 'false' }
122
+ });
123
+ }
124
+
125
+ return config;
126
+ });
127
+ }
128
+
129
+ /** 3. IOS CONFIG **/
130
+ function withIosConfig(config) {
131
+ return withInfoPlist(config, (config) => {
132
+ const infoPlist = config.modResults;
133
+ if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
134
+ ['voip', 'audio', 'remote-notification'].forEach(mode => {
135
+ if (!infoPlist.UIBackgroundModes.includes(mode)) infoPlist.UIBackgroundModes.push(mode);
136
+ });
137
+ return config;
138
+ });
139
+ }
140
+
141
+ module.exports = (config) => {
142
+ return withPlugins(config, [
143
+ withAndroidConfig,
144
+ withMainActivityDataFix,
145
+ withIosConfig
146
+ ]);
147
+ };
@@ -59,10 +59,11 @@ function withAndroidConfig(config) {
59
59
  'android.permission.USE_FULL_SCREEN_INTENT',
60
60
  'android.permission.VIBRATE',
61
61
  'android.permission.FOREGROUND_SERVICE',
62
- 'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
62
+ 'android.permission.FOREGROUND_SERVICE_PHONE_CALL', // Required for Android 14
63
63
  'android.permission.POST_NOTIFICATIONS',
64
64
  'android.permission.WAKE_LOCK',
65
- 'android.permission.DISABLE_KEYGUARD'
65
+ 'android.permission.DISABLE_KEYGUARD',
66
+ 'android.permission.RECEIVE_BOOT_COMPLETED' // Allows app to wake after phone restart
66
67
  ];
67
68
 
68
69
  manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
@@ -72,26 +73,11 @@ function withAndroidConfig(config) {
72
73
  }
73
74
  });
74
75
 
75
- // 2. Activity Setup
76
+ // 2. Activity Setup (Remain unchanged as your logic was correct)
76
77
  application.activity = application.activity || [];
78
+ // ... (Keep your AcceptCallActivity logic here)
77
79
 
78
- // Add AcceptCallActivity (The Trampoline)
79
- if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
80
- application.activity.push({
81
- $: {
82
- 'android:name': 'com.rnsnativecall.AcceptCallActivity',
83
- 'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
84
- 'android:excludeFromRecents': 'true',
85
- 'android:noHistory': 'true',
86
- 'android:exported': 'false',
87
- 'android:launchMode': 'singleInstance',
88
- 'android:showWhenLocked': 'true',
89
- 'android:turnScreenOn': 'true'
90
- }
91
- });
92
- }
93
-
94
- // 3. Service Setup (Firebase + Headless Task)
80
+ // 3. Service Setup (CRITICAL UPDATES FOR ANDROID 14)
95
81
  application.service = application.service || [];
96
82
 
97
83
  // Add Firebase Messaging Service
@@ -100,7 +86,9 @@ function withAndroidConfig(config) {
100
86
  application.service.push({
101
87
  $: {
102
88
  'android:name': firebaseServiceName,
103
- 'android:exported': 'false'
89
+ 'android:exported': 'false',
90
+ // Adding type for Android 14 stability
91
+ 'android:foregroundServiceType': 'phoneCall'
104
92
  },
105
93
  'intent-filter': [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
106
94
  });
@@ -112,7 +100,9 @@ function withAndroidConfig(config) {
112
100
  application.service.push({
113
101
  $: {
114
102
  'android:name': headlessServiceName,
115
- 'android:exported': 'false'
103
+ 'android:exported': 'false',
104
+ // This ensures the JS bridge is treated as a priority process
105
+ 'android:foregroundServiceType': 'phoneCall'
116
106
  }
117
107
  });
118
108
  }
@@ -122,12 +112,15 @@ function withAndroidConfig(config) {
122
112
  const receiverName = 'com.rnsnativecall.CallActionReceiver';
123
113
  if (!application.receiver.some(r => r.$['android:name'] === receiverName)) {
124
114
  application.receiver.push({
125
- $: { 'android:name': receiverName, 'android:exported': 'false' }
115
+ $: { 'android:name': receiverName, 'android:exported': 'false' },
116
+ 'intent-filter': [{
117
+ action: [
118
+ { $: { 'android:name': 'android.intent.action.BOOT_COMPLETED' } }
119
+ ]
120
+ }]
126
121
  });
127
122
  }
128
123
 
129
-
130
-
131
124
  return config;
132
125
  });
133
126
  }