rns-nativecall 0.3.6 → 0.3.9

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
  }
@@ -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
@@ -17,89 +20,104 @@ class CallMessagingService : FirebaseMessagingService() {
17
20
  companion object {
18
21
  private val handler = Handler(Looper.getMainLooper())
19
22
  private val pendingNotifications = mutableMapOf<String, Runnable>()
20
- // This is the standard ID used for the incoming call notification pill
21
- private const val CALL_NOTIFICATION_ID = 123
22
23
  }
23
24
 
24
25
  override fun onMessageReceived(remoteMessage: RemoteMessage) {
25
26
  val data = remoteMessage.data
26
27
  val context = applicationContext
27
-
28
28
  val uuid = data["callUuid"] ?: return
29
29
  val type = data["type"] ?: ""
30
30
 
31
31
  if (type == "CANCEL") {
32
- pendingNotifications[uuid]?.let {
33
- handler.removeCallbacks(it)
34
- pendingNotifications.remove(uuid)
35
- }
36
-
37
- // 1. Stop the ringtone
38
- NativeCallManager.stopRingtone()
39
-
40
- // 2. DISMISS THE PILL (Direct Native Way)
41
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
42
- notificationManager.cancel(CALL_NOTIFICATION_ID)
43
-
44
- // 3. Show Missed Call
45
- showMissedCallNotification(context, data, uuid)
46
- return
47
- }
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
+ }
48
43
 
49
44
  if (isAppInForeground(context)) {
50
45
  CallModule.sendEventToJS("onCallReceived", data)
51
46
  } else {
52
- val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
53
- launchIntent?.apply {
54
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
55
- putExtra("background_wake", true)
56
- }
57
- context.startActivity(launchIntent)
58
-
59
- val showNotificationRunnable = Runnable {
60
- if (!isAppInForeground(context)) {
61
- NativeCallManager.handleIncomingPush(context, data)
62
- }
63
- pendingNotifications.remove(uuid)
64
- }
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)
52
+
53
+ // 2. Start the Service
54
+ context.startService(headlessIntent)
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
+ }
65
62
 
66
- pendingNotifications[uuid] = showNotificationRunnable
67
- handler.postDelayed(showNotificationRunnable, 18000)
63
+ // 4. Start the 18s backup timer
64
+ val showNotificationRunnable = Runnable {
65
+ if (!isAppInForeground(context)) {
66
+ NativeCallManager.handleIncomingPush(context, data)
68
67
  }
68
+ pendingNotifications.remove(uuid)
69
+ }
70
+ pendingNotifications[uuid] = showNotificationRunnable
71
+ handler.postDelayed(showNotificationRunnable, 18000)
72
+ }
69
73
  }
70
74
 
71
75
  private fun showMissedCallNotification(context: Context, data: Map<String, String>, uuid: String) {
72
76
  val name = data["name"] ?: "Unknown"
73
77
  val callType = data["callType"] ?: "video"
74
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() }
75
83
 
76
84
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
77
85
 
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
+
78
100
  var iconResId = context.resources.getIdentifier("ic_missed_call", "drawable", context.packageName)
79
101
  if (iconResId == 0) iconResId = android.R.drawable.sym_call_missed
80
102
 
81
103
  val builder = NotificationCompat.Builder(context, channelId)
82
104
  .setSmallIcon(iconResId)
83
- .setContentTitle("Missed $callType call")
84
- .setContentText("You missed a call from $name")
105
+ .setContentTitle("$capitalizedAppName missed call")
106
+ .setContentText("You missed $article $callType call from $name")
85
107
  .setPriority(NotificationCompat.PRIORITY_HIGH)
86
108
  .setAutoCancel(true)
109
+ .setContentIntent(contentIntent) // Opens app on click
87
110
  .setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
88
111
 
112
+ // 3. Use unique hashCode so multiple missed calls are stacked separately
89
113
  notificationManager.notify(uuid.hashCode(), builder.build())
90
114
  }
91
115
 
92
116
  private fun isAppInForeground(context: Context): Boolean {
93
117
  val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
94
118
  val appProcesses = activityManager.runningAppProcesses ?: return false
95
- val packageName = context.packageName
96
-
97
- for (appProcess in appProcesses) {
98
- if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
99
- appProcess.processName == packageName) {
100
- return true
101
- }
119
+ return appProcesses.any {
120
+ it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && it.processName == context.packageName
102
121
  }
103
- return false
104
122
  }
105
123
  }
@@ -23,7 +23,7 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
23
23
  writableMap
24
24
  }
25
25
  promise.resolve(data)
26
- pendingCallDataMap = null // Clear after use
26
+ pendingCallDataMap = null
27
27
  }
28
28
 
29
29
  @ReactMethod
@@ -48,7 +48,7 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
48
48
  pendingCallDataMap = null
49
49
 
50
50
  val notificationManager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
51
- notificationManager.cancel(101)
51
+ notificationManager.cancel(uuid.hashCode())
52
52
  }
53
53
 
54
54
  @ReactMethod
@@ -14,7 +14,7 @@ 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 CALL_CHANNEL_ID = "CALL_CHANNEL_ID"
18
18
 
19
19
  fun handleIncomingPush(context: Context, data: Map<String, String>) {
20
20
  val uuid = data["callUuid"] ?: return
@@ -28,90 +28,87 @@ object NativeCallManager {
28
28
  } else {
29
29
  PendingIntent.FLAG_UPDATE_CURRENT
30
30
  }
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.
31
+
34
32
  val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
35
33
  action = "ACTION_SHOW_UI_$uuid"
36
34
  data.forEach { (key, value) -> putExtra(key, value) }
37
35
  addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
38
36
  }
39
37
 
40
- val fullScreenPendingIntent = PendingIntent.getActivity(
41
- context,
42
- uuid.hashCode(),
43
- intentToActivity,
44
- pendingFlags
45
- )
38
+ val fullScreenPendingIntent = PendingIntent.getActivity(context, uuid.hashCode(), intentToActivity, pendingFlags)
46
39
 
47
- // 2. Reject Action - Still goes to BroadcastReceiver
48
40
  val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
49
41
  action = "ACTION_REJECT_$uuid"
50
42
  putExtra("EXTRA_CALL_UUID", uuid)
51
- // Pass all data so onReject can find the UUID
52
43
  data.forEach { (key, value) -> putExtra(key, value) }
53
44
  }
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"
45
+
46
+
47
+ val rejectPendingIntent = PendingIntent.getBroadcast(context, uuid.hashCode() + 1, rejectIntent, pendingFlags)
48
+
63
49
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
64
50
 
65
51
  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
52
+ val channel = NotificationChannel(CALL_CHANNEL_ID, "Incoming Call", NotificationManager.IMPORTANCE_HIGH).apply {
71
53
  setBypassDnd(true)
72
54
  lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
73
- // On Android O+, sound should be set on the channel
55
+ enableVibration(true) //can be removed
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)
84
- .setPriority(NotificationCompat.PRIORITY_MAX)
65
+
66
+ .setPriority(NotificationCompat.PRIORITY_HIGH) //old was PRIORITY_MAX
85
67
  .setCategory(NotificationCompat.CATEGORY_CALL)
86
68
  .setOngoing(true)
87
69
  .setAutoCancel(false)
88
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
89
70
 
90
- // This is the key for the "Pill" / Heads-up display
91
- .setFullScreenIntent(fullScreenPendingIntent, true)
92
-
93
- .addAction(0, "Answer", fullScreenPendingIntent) // Same intent as fullScreen
71
+ .setFullScreenIntent(fullScreenPendingIntent, false) //can be true
72
+ .addAction(0, "Answer", fullScreenPendingIntent)
94
73
  .addAction(0, "Decline", rejectPendingIntent)
95
74
 
96
- notificationManager.notify(NOTIFICATION_ID, builder.build())
75
+ notificationManager.notify(uuid.hashCode(), builder.build())
97
76
 
98
- // 5. Ringtone with looping logic attempt
99
77
  try {
100
78
  val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
101
79
  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
- }
80
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) ringtone?.isLooping = true
105
81
  ringtone?.play()
106
- } catch (e: Exception) {
107
- e.printStackTrace()
108
- }
82
+ } catch (e: Exception) { e.printStackTrace() }
109
83
  }
110
84
 
111
85
  fun stopRingtone() {
112
- if (ringtone?.isPlaying == true) {
113
- ringtone?.stop()
86
+ try {
87
+ // Use the safe call operator ?. to only run if ringtone isn't null
88
+ ringtone?.let {
89
+ if (it.isPlaying) {
90
+ it.stop()
91
+ }
114
92
  }
93
+ // Always null it out after stopping
115
94
  ringtone = null
95
+ } catch (e: Exception) {
96
+ // Prevent the app from crashing if the system Ringtone service is acting up
97
+ e.printStackTrace()
98
+ ringtone = null
99
+ }
100
+ }
101
+
102
+ fun dismissIncomingCall(context: Context, uuid: String?) {
103
+ stopRingtone()
104
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
105
+
106
+ if (uuid != null) {
107
+ notificationManager.cancel(uuid.hashCode())
108
+ } else {
109
+ // Fallback: If for some reason uuid is null, cancel everything
110
+ // (Optional: only if you want a safety net)
111
+ // notificationManager.cancelAll()
112
+ }
116
113
  }
117
114
  }
package/app.plugin.js CHANGED
@@ -1,6 +1,13 @@
1
- ////Users/bush/Desktop/Apps/Raiidr/package/app.plugin.js
1
+ ///app.plugin.js
2
+ const { withPlugins } = require('@expo/config-plugins');
2
3
  const withNativeCallVoip = require('./withNativeCallVoip');
4
+ const withCallNativeConfig = require('./withCallNativeConfig');
5
+ const withCallPermissions = require('./withCallPermissions');
3
6
 
4
- module.exports = function (config) {
5
- return withNativeCallVoip(config);
7
+ module.exports = function (config, props) {
8
+ return withPlugins(config, [
9
+ [withNativeCallVoip, props],
10
+ [withCallNativeConfig, props],
11
+ [withCallPermissions, props],
12
+ ]);
6
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.3.6",
3
+ "version": "0.3.9",
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,8 @@
43
43
  "react-native.config.js",
44
44
  "README.md",
45
45
  "rns-nativecall.podspec",
46
+ "withCallNativeConfig.js",
47
+ "withCallPermissions.js",
46
48
  "withNativeCallVoip.js"
47
49
  ],
48
50
  "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
+ };
@@ -0,0 +1,40 @@
1
+ const { withAndroidManifest } = require("@expo/config-plugins");
2
+
3
+ /** 2. ANDROID MANIFEST MOD **/
4
+ function withCallPermissions(config) {
5
+ return withAndroidManifest(config, async (config) => {
6
+ const androidManifest = config.modResults.manifest;
7
+
8
+ // 1. Add the Full Screen Intent Permission
9
+ if (!androidManifest["uses-permission"]) {
10
+ androidManifest["uses-permission"] = [];
11
+ }
12
+
13
+ const hasPermission = androidManifest["uses-permission"].some(
14
+ (p) => p.$["android:name"] === "android.permission.USE_FULL_SCREEN_INTENT"
15
+ );
16
+
17
+ if (!hasPermission) {
18
+ androidManifest["uses-permission"].push({
19
+ $: { "android:name": "android.permission.USE_FULL_SCREEN_INTENT" },
20
+ });
21
+ }
22
+
23
+ // 2. Ensure the Activity has the correct flags to appear over lockscreen
24
+ const mainApplication = androidManifest.application[0];
25
+ const mainActivity = mainApplication.activity.find(
26
+ (a) => a.$["android:name"] === ".MainActivity"
27
+ );
28
+
29
+ if (mainActivity) {
30
+ mainActivity.$["android:showOnLockScreen"] = "true";
31
+ mainActivity.$["android:lockTaskMode"] = "if_whitelisted";
32
+ }
33
+
34
+ return config;
35
+ });
36
+ }
37
+
38
+ module.exports = (config) => {
39
+ return withPlugins(config, [withCallPermissions]);
40
+ };
@@ -1,46 +1,140 @@
1
1
  const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity } = require('@expo/config-plugins');
2
2
 
3
+ /** 1. ANDROID MAIN ACTIVITY MOD **/
4
+ // function withMainActivityDataFix(config) {
5
+ // return withMainActivity(config, (config) => {
6
+ // let contents = config.modResults.contents;
7
+
8
+ // // 1. Ensure necessary Imports are present
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
+ // // 2. Define the code blocks
17
+ // const onNewIntentCode = `
18
+ // override fun onNewIntent(intent: Intent) {
19
+ // super.onNewIntent(intent)
20
+ // setIntent(intent)
21
+ // }
22
+ // `;
23
+
24
+ // const onCreateCode = `
25
+ // override fun onCreate(savedInstanceState: Bundle?) {
26
+ // super.onCreate(savedInstanceState)
27
+ // // If woken up silently, move to back to prevent UI flicker
28
+ // if (intent.getBooleanExtra("background_wake", false)) {
29
+ // moveTaskToBack(true)
30
+ // }
31
+ // }
32
+ // `;
33
+
34
+ // // 3. Insert the codes before the last closing brace of the class
35
+ // if (!contents.includes('override fun onNewIntent')) {
36
+ // const lastBraceIndex = contents.lastIndexOf('}');
37
+ // contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
38
+ // }
39
+
40
+ // if (!contents.includes('override fun onCreate')) {
41
+ // // Re-calculate lastBraceIndex because contents string has changed
42
+ // const lastBraceIndex = contents.lastIndexOf('}');
43
+ // contents = contents.slice(0, lastBraceIndex) + onCreateCode + contents.slice(lastBraceIndex);
44
+ // }
45
+
46
+ // config.modResults.contents = contents;
47
+ // return config;
48
+ // });
49
+ // }
50
+
3
51
  /** 1. ANDROID MAIN ACTIVITY MOD **/
4
52
  function withMainActivityDataFix(config) {
5
53
  return withMainActivity(config, (config) => {
6
54
  let contents = config.modResults.contents;
7
55
 
8
- // 1. Ensure necessary Imports are present
9
- if (!contents.includes('import android.content.Intent')) {
10
- contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
56
+ // --- 1. Imports ---
57
+ const neededImports = [
58
+ 'import android.view.WindowManager',
59
+ 'import android.os.Build',
60
+ 'import android.os.Bundle'
61
+ ];
62
+
63
+ neededImports.forEach(imp => {
64
+ if (!contents.includes(imp)) {
65
+ contents = contents.replace(/package .*/, (match) => `${match}\n${imp}`);
66
+ }
67
+ });
68
+
69
+ // --- 2. Replace onCreate ---
70
+ // We look for the existing onCreate and replace the entire block with your version
71
+ const editedOnCreate = `
72
+ // override fun onCreate(savedInstanceState: Bundle?) {
73
+ // setTheme(R.style.AppTheme)
74
+ // super.onCreate(savedInstanceState)
75
+
76
+ // // --- LOCK SCREEN WAKE LOGIC ---
77
+ // // This allows the Activity to appear over the lock screen when Answer is pressed
78
+ // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
79
+ // setShowWhenLocked(true)
80
+ // setTurnScreenOn(true)
81
+ // } else {
82
+ // @Suppress("DEPRECATION")
83
+ // window.addFlags(
84
+ // WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
85
+ // WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
86
+ // WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
87
+ // )
88
+ // }
89
+ // }
90
+ override fun onCreate(savedInstanceState: Bundle?) {
91
+ setTheme(R.style.AppTheme)
92
+ super.onCreate(savedInstanceState)
93
+
94
+ // Only wake the screen/bypass lock if we are coming from a notification interaction
95
+ // Otherwise, let the notification pill handle the UI.
96
+ if (intent?.action?.startsWith("ACTION_SHOW_UI") == true) {
97
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
98
+ setShowWhenLocked(true)
99
+ setTurnScreenOn(true)
100
+ } else {
101
+ @Suppress("DEPRECATION")
102
+ window.addFlags(
103
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
104
+ WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
105
+ WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
106
+ )
107
+ }
11
108
  }
12
- if (!contents.includes('import android.os.Bundle')) {
13
- contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.os.Bundle`);
109
+ }
110
+ `;
111
+
112
+
113
+
114
+
115
+ // Regex to find the existing onCreate block (including the setTheme and super calls)
116
+ const onCreateRegex = /override fun onCreate\(savedInstanceState: Bundle\?\).*?\{[\s\S]*?super\.onCreate\(null\)\n\s*\}/;
117
+ if (contents.match(onCreateRegex)) {
118
+ contents = contents.replace(onCreateRegex, editedOnCreate);
14
119
  }
15
120
 
16
- // 2. Define the code blocks
121
+ // --- 3. Update createReactActivityDelegate ---
122
+ // Removing the "object :" and the empty trailing block "{})" to match your edited version
123
+ contents = contents.replace(
124
+ /return ReactActivityDelegateWrapper\([\s\S]*?object : (DefaultReactActivityDelegate\([\s\S]*?\))\{\}\)/,
125
+ 'return ReactActivityDelegateWrapper($1)'
126
+ );
127
+
128
+ // --- 4. Ensure onNewIntent exists ---
17
129
  const onNewIntentCode = `
18
- override fun onNewIntent(intent: Intent) {
19
- super.onNewIntent(intent)
20
- setIntent(intent)
21
- }
22
- `;
23
-
24
- const onCreateCode = `
25
- override fun onCreate(savedInstanceState: Bundle?) {
26
- super.onCreate(savedInstanceState)
27
- // If woken up silently, move to back to prevent UI flicker
28
- if (intent.getBooleanExtra("background_wake", false)) {
29
- moveTaskToBack(true)
30
- }
31
- }
32
- `;
130
+ override fun onNewIntent(intent: Intent) {
131
+ super.onNewIntent(intent)
132
+ setIntent(intent)
133
+ }`;
33
134
 
34
- // 3. Insert the codes before the last closing brace of the class
35
135
  if (!contents.includes('override fun onNewIntent')) {
36
136
  const lastBraceIndex = contents.lastIndexOf('}');
37
- contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
38
- }
39
-
40
- if (!contents.includes('override fun onCreate')) {
41
- // Re-calculate lastBraceIndex because contents string has changed
42
- const lastBraceIndex = contents.lastIndexOf('}');
43
- contents = contents.slice(0, lastBraceIndex) + onCreateCode + contents.slice(lastBraceIndex);
137
+ contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + "\n" + contents.slice(lastBraceIndex);
44
138
  }
45
139
 
46
140
  config.modResults.contents = contents;
@@ -59,10 +153,11 @@ function withAndroidConfig(config) {
59
153
  'android.permission.USE_FULL_SCREEN_INTENT',
60
154
  'android.permission.VIBRATE',
61
155
  'android.permission.FOREGROUND_SERVICE',
62
- 'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
156
+ 'android.permission.FOREGROUND_SERVICE_PHONE_CALL', // Required for Android 14
63
157
  'android.permission.POST_NOTIFICATIONS',
64
158
  'android.permission.WAKE_LOCK',
65
- 'android.permission.DISABLE_KEYGUARD'
159
+ 'android.permission.DISABLE_KEYGUARD',
160
+ 'android.permission.RECEIVE_BOOT_COMPLETED' // Allows app to wake after phone restart
66
161
  ];
67
162
 
68
163
  manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
@@ -72,26 +167,11 @@ function withAndroidConfig(config) {
72
167
  }
73
168
  });
74
169
 
75
- // 2. Activity Setup
170
+ // 2. Activity Setup (Remain unchanged as your logic was correct)
76
171
  application.activity = application.activity || [];
172
+ // ... (Keep your AcceptCallActivity logic here)
77
173
 
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)
174
+ // 3. Service Setup (CRITICAL UPDATES FOR ANDROID 14)
95
175
  application.service = application.service || [];
96
176
 
97
177
  // Add Firebase Messaging Service
@@ -100,7 +180,9 @@ function withAndroidConfig(config) {
100
180
  application.service.push({
101
181
  $: {
102
182
  'android:name': firebaseServiceName,
103
- 'android:exported': 'false'
183
+ 'android:exported': 'false',
184
+ // Adding type for Android 14 stability
185
+ 'android:foregroundServiceType': 'phoneCall'
104
186
  },
105
187
  'intent-filter': [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
106
188
  });
@@ -112,7 +194,9 @@ function withAndroidConfig(config) {
112
194
  application.service.push({
113
195
  $: {
114
196
  'android:name': headlessServiceName,
115
- 'android:exported': 'false'
197
+ 'android:exported': 'false',
198
+ // This ensures the JS bridge is treated as a priority process
199
+ 'android:foregroundServiceType': 'phoneCall'
116
200
  }
117
201
  });
118
202
  }
@@ -122,12 +206,15 @@ function withAndroidConfig(config) {
122
206
  const receiverName = 'com.rnsnativecall.CallActionReceiver';
123
207
  if (!application.receiver.some(r => r.$['android:name'] === receiverName)) {
124
208
  application.receiver.push({
125
- $: { 'android:name': receiverName, 'android:exported': 'false' }
209
+ $: { 'android:name': receiverName, 'android:exported': 'false' },
210
+ 'intent-filter': [{
211
+ action: [
212
+ { $: { 'android:name': 'android.intent.action.BOOT_COMPLETED' } }
213
+ ]
214
+ }]
126
215
  });
127
216
  }
128
217
 
129
-
130
-
131
218
  return config;
132
219
  });
133
220
  }