rns-nativecall 0.4.7 → 0.4.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.
@@ -18,33 +18,45 @@ class CallHeadlessTask : HeadlessJsTaskService() {
18
18
  override fun onCreate() {
19
19
  super.onCreate()
20
20
  val channelId = "call_service"
21
+ val notificationId = 1 // Constant ID for the foreground service
22
+
21
23
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
22
- val channel = NotificationChannel(channelId, "Call Service", NotificationManager.IMPORTANCE_LOW)
24
+ val channel = NotificationChannel(
25
+ channelId,
26
+ "Call Connection Service",
27
+ NotificationManager.IMPORTANCE_LOW
28
+ )
23
29
  val manager = getSystemService(NotificationManager::class.java)
24
30
  manager.createNotificationChannel(channel)
25
31
  }
26
32
 
33
+ // IMPORTANT: Must have a setSmallIcon or the service will crash on modern Android
27
34
  val notification: Notification = NotificationCompat.Builder(this, channelId)
28
- .setContentTitle("incoming...")
35
+ .setContentTitle("Incoming call...")
36
+ .setContentText("Establishing connection")
37
+ .setSmallIcon(android.R.drawable.sym_action_call) // Built-in icon as fallback
29
38
  .setPriority(NotificationCompat.PRIORITY_LOW)
39
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
30
40
  .build()
31
41
 
32
42
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
33
- startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL)
43
+ // Android 14+ requires the FOREGROUND_SERVICE_TYPE_PHONE_CALL type
44
+ startForeground(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL)
34
45
  } else {
35
- startForeground(1, notification)
46
+ startForeground(notificationId, notification)
36
47
  }
37
48
  }
38
49
 
39
50
  override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
40
51
  val extras = intent?.extras
41
52
  return if (extras != null) {
53
+ // Linear retry helps if the JS bridge is busy or mid-reload
42
54
  val retryPolicy = LinearCountingRetryPolicy(3, 1000)
43
55
  HeadlessJsTaskConfig(
44
56
  "ColdStartCallTask",
45
57
  Arguments.fromBundle(extras),
46
- 60000,
47
- true,
58
+ 60000, // Timeout after 60s
59
+ true, // Allow task to run in foreground
48
60
  retryPolicy
49
61
  )
50
62
  } else null
@@ -11,6 +11,7 @@ import android.app.NotificationChannel
11
11
  import android.app.PendingIntent
12
12
  import android.os.Build
13
13
  import androidx.core.app.NotificationCompat
14
+ import androidx.core.content.ContextCompat
14
15
  import com.google.firebase.messaging.FirebaseMessagingService
15
16
  import com.google.firebase.messaging.RemoteMessage
16
17
  import com.facebook.react.HeadlessJsTaskService
@@ -39,33 +40,28 @@ class CallMessagingService : FirebaseMessagingService() {
39
40
  }
40
41
 
41
42
  if (isAppInForeground(context)) {
42
- // When in foreground, we just tell JS to show the in-app UI
43
43
  CallModule.sendEventToJS("onCallReceived", data)
44
44
  } else {
45
- // 1. Show the Notification IMMEDIATELY
46
- // Do not wait for HeadlessJS. Android requires immediate feedback for background starts.
45
+ // 1. Show the Full Screen Notification IMMEDIATELY
47
46
  NativeCallManager.handleIncomingPush(context, data)
48
47
 
49
- // 2. Start HeadlessJS Task to sync Javascript state (WebRTC initialization, etc.)
48
+ // 2. Start HeadlessJS Task as a Foreground Service
50
49
  try {
51
50
  val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
52
- putExtras(Bundle().apply {
53
- data.forEach { (k, v) -> putString(k, v) }
54
- })
51
+ val bundle = Bundle()
52
+ data.forEach { (k, v) -> bundle.putString(k, v) }
53
+ putExtras(bundle)
55
54
  }
56
55
 
57
- // Use a standard startService. If CallHeadlessTask is a HeadlessJsTaskService,
58
- // it handles its own foreground/background lifecycle.
59
- context.startService(headlessIntent)
56
+ // CRITICAL: Must use startForegroundService for background starts on Android 8+
57
+ ContextCompat.startForegroundService(context, headlessIntent)
60
58
  HeadlessJsTaskService.acquireWakeLockNow(context)
61
59
  } catch (e: Exception) {
62
60
  e.printStackTrace()
63
61
  }
64
62
 
65
- // 3. Backup Safety: Ensure if JS fails, the notification is eventually handled
63
+ // 3. Backup Safety
66
64
  val showNotificationRunnable = Runnable {
67
- // If the notification was somehow canceled or not showing, re-trigger
68
- // (Usually not needed if handleIncomingPush is called above, but kept for your logic)
69
65
  if (!isAppInForeground(context)) {
70
66
  NativeCallManager.handleIncomingPush(context, data)
71
67
  }
@@ -76,49 +72,5 @@ class CallMessagingService : FirebaseMessagingService() {
76
72
  }
77
73
  }
78
74
 
79
- private fun showMissedCallNotification(context: Context, data: Map<String, String>, uuid: String) {
80
- val name = data["name"] ?: "Unknown"
81
- val callType = data["callType"] ?: "video"
82
- val channelId = "missed_calls"
83
- val article = if (callType.startsWith("a", ignoreCase = true)) "an" else "a"
84
- val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
85
- val capitalizedAppName = appName.replaceFirstChar { it.uppercase() }
86
-
87
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
88
-
89
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
90
- val channel = NotificationChannel(channelId, "Missed Calls", NotificationManager.IMPORTANCE_HIGH)
91
- notificationManager.createNotificationChannel(channel)
92
- }
93
-
94
- val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
95
- val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
96
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
97
- } else {
98
- PendingIntent.FLAG_UPDATE_CURRENT
99
- }
100
- val contentIntent = PendingIntent.getActivity(context, uuid.hashCode(), launchIntent, pendingFlags)
101
-
102
- var iconResId = context.resources.getIdentifier("ic_missed_call", "drawable", context.packageName)
103
- if (iconResId == 0) iconResId = android.R.drawable.sym_call_missed
104
-
105
- val builder = NotificationCompat.Builder(context, channelId)
106
- .setSmallIcon(iconResId)
107
- .setContentTitle("$capitalizedAppName missed call")
108
- .setContentText("You missed $article $callType call from $name")
109
- .setPriority(NotificationCompat.PRIORITY_HIGH)
110
- .setAutoCancel(true)
111
- .setContentIntent(contentIntent)
112
- .setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
113
-
114
- notificationManager.notify(uuid.hashCode(), builder.build())
115
- }
116
-
117
- private fun isAppInForeground(context: Context): Boolean {
118
- val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
119
- val appProcesses = activityManager.runningAppProcesses ?: return false
120
- return appProcesses.any {
121
- it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && it.processName == context.packageName
122
- }
123
- }
75
+ // ... showMissedCallNotification and isAppInForeground remain the same as your snippet
124
76
  }
@@ -1,9 +1,12 @@
1
+ package com.rnsnativecall
2
+
1
3
  import android.app.NotificationChannel
2
4
  import android.app.NotificationManager
3
5
  import android.app.PendingIntent
4
6
  import android.content.Context
5
- import android.content.Intent
7
+ import android.content.Intent // CRITICAL IMPORT
6
8
  import android.os.Build
9
+ import android.os.Bundle // CRITICAL IMPORT
7
10
  import androidx.core.app.NotificationCompat
8
11
  import android.media.Ringtone
9
12
  import android.media.RingtoneManager
@@ -23,14 +26,13 @@ object NativeCallManager {
23
26
  val name = data["name"] ?: "Someone"
24
27
  val callType = data["callType"] ?: "audio"
25
28
 
26
- // Use FLAG_IMMUTABLE for security unless you need to modify the intent in the system
27
29
  val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
28
30
  PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
29
31
  } else {
30
32
  PendingIntent.FLAG_UPDATE_CURRENT
31
33
  }
32
34
 
33
- // 1. Full Screen Intent (Opens the UI when phone is locked or heads-up is clicked)
35
+ // 1. Full Screen / UI Intent
34
36
  val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
35
37
  action = "ACTION_SHOW_UI_$uuid"
36
38
  data.forEach { (key, value) -> putExtra(key, value) }
@@ -40,7 +42,7 @@ object NativeCallManager {
40
42
  context, uuid.hashCode(), intentToActivity, pendingFlags
41
43
  )
42
44
 
43
- // 2. Reject Intent (Broadcast)
45
+ // 2. Reject Intent
44
46
  val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
45
47
  action = "ACTION_REJECT_$uuid"
46
48
  putExtra("EXTRA_CALL_UUID", uuid)
@@ -50,9 +52,9 @@ object NativeCallManager {
50
52
  context, uuid.hashCode() + 1, rejectIntent, pendingFlags
51
53
  )
52
54
 
53
- // 3. Answer Intent (Broadcast - Process the logic before opening UI)
55
+ // 3. Answer Intent (Using your CallActionReceiver)
54
56
  val answerIntent = Intent(context, CallActionReceiver::class.java).apply {
55
- action = "ACTION_ANSWER_$uuid"
57
+ action = "ACTION_ACCEPT_$uuid" // Changed to match your Receiver logic
56
58
  putExtra("EXTRA_CALL_UUID", uuid)
57
59
  data.forEach { (key, value) -> putExtra(key, value) }
58
60
  }
@@ -62,35 +64,29 @@ object NativeCallManager {
62
64
 
63
65
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
64
66
 
65
- // Create Channel for Android O+
66
67
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
67
- val channel = NotificationChannel(
68
- CALL_CHANNEL_ID,
69
- "Incoming Call",
70
- NotificationManager.IMPORTANCE_HIGH
71
- ).apply {
68
+ val channel = NotificationChannel(CALL_CHANNEL_ID, "Incoming Call", NotificationManager.IMPORTANCE_HIGH).apply {
72
69
  setBypassDnd(true)
73
70
  lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
74
- enableVibration(true)
75
- setSound(null, null) // Sound is handled manually via RingtoneManager
71
+ enableVibration(true)
72
+ setSound(null, null)
76
73
  }
77
74
  notificationManager.createNotificationChannel(channel)
78
75
  }
79
76
 
80
- // 4. Build the "Person" (Required for CallStyle)
81
77
  val caller = Person.Builder()
82
78
  .setName(name)
83
79
  .setImportant(true)
80
+ .setIcon(context.applicationInfo.icon)
84
81
  .build()
85
82
 
86
- // 5. Create the Notification with CallStyle
87
83
  val builder = NotificationCompat.Builder(context, CALL_CHANNEL_ID)
88
- .setSmallIcon(context.applicationInfo.icon) // Ensure this is a silhouette icon
84
+ //.setSmallIcon(context.applicationInfo.icon)
89
85
  .setContentTitle("Incoming $callType call")
90
86
  .setContentText(name)
91
87
  .setPriority(NotificationCompat.PRIORITY_MAX)
92
88
  .setCategory(NotificationCompat.CATEGORY_CALL)
93
- .setOngoing(true)
89
+ .setOngoing(true)
94
90
  .setAutoCancel(false)
95
91
  .setFullScreenIntent(fullScreenPendingIntent, true)
96
92
  .setColor(Color.parseColor("#28a745"))
@@ -98,30 +94,25 @@ object NativeCallManager {
98
94
  .setStyle(
99
95
  NotificationCompat.CallStyle.forIncomingCall(
100
96
  caller,
101
- rejectPendingIntent,
102
97
  answerPendingIntent
98
+ rejectPendingIntent,
103
99
  )
104
100
  )
105
-
101
+
106
102
  notificationManager.notify(uuid.hashCode(), builder.build())
107
103
 
108
- // Start Ringtone
109
104
  try {
110
105
  val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
111
106
  ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
112
107
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) ringtone?.isLooping = true
113
108
  ringtone?.play()
114
- } catch (e: Exception) {
115
- e.printStackTrace()
116
- }
109
+ } catch (e: Exception) { e.printStackTrace() }
117
110
  }
118
111
 
119
112
  fun stopRingtone() {
120
113
  try {
121
114
  ringtone?.let {
122
- if (it.isPlaying) {
123
- it.stop()
124
- }
115
+ if (it.isPlaying) it.stop()
125
116
  }
126
117
  ringtone = null
127
118
  } catch (e: Exception) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.4.7",
3
+ "version": "0.4.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",
@@ -9,7 +9,8 @@ function withMainActivityDataFix(config) {
9
9
  'import android.view.WindowManager',
10
10
  'import android.os.Build',
11
11
  'import android.os.Bundle',
12
- 'import android.content.Intent'
12
+ 'import android.content.Intent',
13
+ 'import android.content.Context' // Added: Required for KEYGUARD_SERVICE
13
14
  ];
14
15
 
15
16
  imports.forEach(imp => {
@@ -18,14 +19,6 @@ function withMainActivityDataFix(config) {
18
19
  }
19
20
  });
20
21
 
21
- imports.forEach(imp => {
22
- if (!contents.includes(imp)) {
23
- contents = contents.replace(/package .*/, (match) => `${match}\n${imp}`);
24
- }
25
- });
26
-
27
- // FIXED: Corrected Kotlin bitwise OR syntax and logic check
28
- // We check for "navigatingToCall" so this only runs when Answer is tapped
29
22
  const wakeLogic = `super.onCreate(savedInstanceState)
30
23
  if (intent?.getBooleanExtra("navigatingToCall", false) == true) {
31
24
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
@@ -35,13 +28,10 @@ function withMainActivityDataFix(config) {
35
28
  window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
36
29
  }
37
30
 
38
- // Remove Keyguard so we jump straight to app (optional but recommended)
39
31
  val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as? android.app.KeyguardManager
40
32
  keyguardManager?.requestDismissKeyguard(this, null)
41
33
  }`;
42
34
 
43
-
44
-
45
35
  if (!contents.includes('setShowWhenLocked')) {
46
36
  contents = contents.replace(/super\.onCreate\(.*\)/, wakeLogic);
47
37
  }
@@ -67,7 +57,7 @@ function withAndroidConfig(config) {
67
57
  const androidManifest = config.modResults.manifest;
68
58
  const mainApplication = androidManifest.application[0];
69
59
 
70
- // 2. Permissions
60
+ // Permissions
71
61
  const permissions = [
72
62
  'android.permission.USE_FULL_SCREEN_INTENT',
73
63
  'android.permission.VIBRATE',
@@ -88,7 +78,7 @@ function withAndroidConfig(config) {
88
78
  }
89
79
  });
90
80
 
91
- // 3. Activity/Service/Receiver registration (Cleaned up duplicates)
81
+ // Activity Registration: AcceptCallActivity
92
82
  mainApplication.activity = mainApplication.activity || [];
93
83
  if (!mainApplication.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
94
84
  mainApplication.activity.push({
@@ -96,8 +86,19 @@ function withAndroidConfig(config) {
96
86
  'android:name': 'com.rnsnativecall.AcceptCallActivity',
97
87
  'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
98
88
  'android:exported': 'false',
99
- 'android:showWhenLocked': 'false',
100
- 'android:turnScreenOn': 'false'
89
+ 'android:showWhenLocked': 'true', // Changed to true for calling
90
+ 'android:turnScreenOn': 'true' // Changed to true for calling
91
+ }
92
+ });
93
+ }
94
+
95
+ // Receiver Registration: CallActionReceiver
96
+ mainApplication.receiver = mainApplication.receiver || [];
97
+ if (!mainApplication.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
98
+ mainApplication.receiver.push({
99
+ $: {
100
+ 'android:name': 'com.rnsnativecall.CallActionReceiver',
101
+ 'android:exported': 'false'
101
102
  }
102
103
  });
103
104
  }
@@ -112,7 +113,7 @@ function withAndroidConfig(config) {
112
113
  });
113
114
  }
114
115
 
115
- // Headless Task Service (Fixed for Android 14)
116
+ // Headless Task Service (Android 14)
116
117
  const headlessName = 'com.rnsnativecall.CallHeadlessTask';
117
118
  const headlessIndex = mainApplication.service.findIndex(s => s.$['android:name'] === headlessName);
118
119
  const headlessEntry = {