rns-nativecall 0.4.2 → 0.4.3

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.
@@ -1,23 +1,53 @@
1
1
  package com.rnsnativecall
2
2
 
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.content.Context
3
7
  import android.content.Intent
8
+ import android.content.pm.ServiceInfo
9
+ import android.os.Build
10
+ import androidx.core.app.NotificationCompat
4
11
  import com.facebook.react.HeadlessJsTaskService
5
12
  import com.facebook.react.bridge.Arguments
6
13
  import com.facebook.react.jstasks.HeadlessJsTaskConfig
14
+ import com.facebook.react.jstasks.LinearCountingRetryPolicy
7
15
 
8
16
  class CallHeadlessTask : HeadlessJsTaskService() {
9
- // Note: The '?' after Intent and the return type are specific in Kotlin
17
+
18
+ override fun onCreate() {
19
+ super.onCreate()
20
+ val channelId = "call_service"
21
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
22
+ val channel = NotificationChannel(channelId, "Call Service", NotificationManager.IMPORTANCE_LOW)
23
+ val manager = getSystemService(NotificationManager::class.java)
24
+ manager.createNotificationChannel(channel)
25
+ }
26
+
27
+ val notification: Notification = NotificationCompat.Builder(this, channelId)
28
+ .setContentTitle("Incoming Call")
29
+ .setSmallIcon(android.R.drawable.sym_call_incoming)
30
+ .setPriority(NotificationCompat.PRIORITY_LOW)
31
+ .build()
32
+
33
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
34
+ startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL)
35
+ } else {
36
+ startForeground(1, notification)
37
+ }
38
+ }
39
+
10
40
  override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
11
41
  val extras = intent?.extras
12
42
  return if (extras != null) {
43
+ val retryPolicy = LinearCountingRetryPolicy(3, 1000)
13
44
  HeadlessJsTaskConfig(
14
45
  "ColdStartCallTask",
15
46
  Arguments.fromBundle(extras),
16
- 30000,
17
- true
47
+ 60000,
48
+ true,
49
+ retryPolicy
18
50
  )
19
- } else {
20
- null
21
- }
51
+ } else null
22
52
  }
23
53
  }
@@ -29,47 +29,47 @@ class CallMessagingService : FirebaseMessagingService() {
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
- // 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
- }
32
+ pendingNotifications[uuid]?.let {
33
+ handler.removeCallbacks(it)
34
+ pendingNotifications.remove(uuid)
35
+ }
36
+ NativeCallManager.dismissIncomingCall(context, uuid)
37
+ showMissedCallNotification(context, data, uuid)
38
+ return
39
+ }
43
40
 
44
41
  if (isAppInForeground(context)) {
45
42
  CallModule.sendEventToJS("onCallReceived", data)
46
43
  } else {
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
- }
44
+ // 1. Prepare Intent
45
+ val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
46
+ putExtras(Bundle().apply {
47
+ data.forEach { (k, v) -> putString(k, v) }
48
+ })
49
+ }
62
50
 
63
- // 4. Start the 18s backup timer
64
- val showNotificationRunnable = Runnable {
65
- if (!isAppInForeground(context)) {
66
- NativeCallManager.handleIncomingPush(context, data)
51
+ // 2. Start Service correctly based on SDK
52
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
53
+ context.startForegroundService(headlessIntent)
54
+ } else {
55
+ context.startService(headlessIntent)
56
+ }
57
+
58
+ // 3. WakeLock
59
+ try {
60
+ HeadlessJsTaskService.acquireWakeLockNow(context)
61
+ } catch (e: Exception) { e.printStackTrace() }
62
+
63
+ // 4. Backup Timer
64
+ val showNotificationRunnable = Runnable {
65
+ if (!isAppInForeground(context)) {
66
+ NativeCallManager.handleIncomingPush(context, data)
67
+ }
68
+ pendingNotifications.remove(uuid)
69
+ }
70
+ pendingNotifications[uuid] = showNotificationRunnable
71
+ handler.postDelayed(showNotificationRunnable, 18000)
67
72
  }
68
- pendingNotifications.remove(uuid)
69
- }
70
- pendingNotifications[uuid] = showNotificationRunnable
71
- handler.postDelayed(showNotificationRunnable, 18000)
72
- }
73
73
  }
74
74
 
75
75
  private fun showMissedCallNotification(context: Context, data: Map<String, String>, uuid: String) {
@@ -77,9 +77,8 @@ class CallMessagingService : FirebaseMessagingService() {
77
77
  val callType = data["callType"] ?: "video"
78
78
  val channelId = "missed_calls"
79
79
  val article = if (callType.startsWith("a", ignoreCase = true)) "an" else "a"
80
- // 1. Format App Name: Capitalized
81
80
  val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
82
- val capitalizedAppName = appName.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
81
+ val capitalizedAppName = appName.replaceFirstChar { it.uppercase() }
83
82
 
84
83
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
85
84
 
@@ -88,7 +87,6 @@ class CallMessagingService : FirebaseMessagingService() {
88
87
  notificationManager.createNotificationChannel(channel)
89
88
  }
90
89
 
91
- // 2. Create Intent to open app when clicking missed call
92
90
  val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
93
91
  val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
94
92
  PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
@@ -106,10 +104,9 @@ class CallMessagingService : FirebaseMessagingService() {
106
104
  .setContentText("You missed $article $callType call from $name")
107
105
  .setPriority(NotificationCompat.PRIORITY_HIGH)
108
106
  .setAutoCancel(true)
109
- .setContentIntent(contentIntent) // Opens app on click
107
+ .setContentIntent(contentIntent)
110
108
  .setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
111
109
 
112
- // 3. Use unique hashCode so multiple missed calls are stacked separately
113
110
  notificationManager.notify(uuid.hashCode(), builder.build())
114
111
  }
115
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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,8 +43,6 @@
43
43
  "react-native.config.js",
44
44
  "README.md",
45
45
  "rns-nativecall.podspec",
46
- "withCallNativeConfig.js",
47
- "withCallPermissions.js",
48
46
  "withNativeCallVoip.js"
49
47
  ],
50
48
  "peerDependencies": {
@@ -5,20 +5,20 @@ function withMainActivityDataFix(config) {
5
5
  return withMainActivity(config, (config) => {
6
6
  let contents = config.modResults.contents;
7
7
 
8
- // 1. Add required imports
9
8
  const imports = [
10
9
  'import android.view.WindowManager',
11
10
  'import android.os.Build',
12
11
  'import android.os.Bundle',
13
12
  'import android.content.Intent'
14
13
  ];
14
+
15
15
  imports.forEach(imp => {
16
16
  if (!contents.includes(imp)) {
17
17
  contents = contents.replace(/package .*/, (match) => `${match}\n${imp}`);
18
18
  }
19
19
  });
20
20
 
21
- // 2. Add the Lockscreen / Wake logic inside onCreate
21
+ // FIXED: Corrected Kotlin bitwise OR syntax and logic check
22
22
  const wakeLogic = `super.onCreate(savedInstanceState)
23
23
  if (intent?.getBooleanExtra("navigatingToCall", false) == true) {
24
24
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
@@ -33,7 +33,6 @@ function withMainActivityDataFix(config) {
33
33
  contents = contents.replace(/super\.onCreate\(.*\)/, wakeLogic);
34
34
  }
35
35
 
36
- // 3. Ensure onNewIntent is present
37
36
  if (!contents.includes('override fun onNewIntent')) {
38
37
  const onNewIntentCode = `
39
38
  override fun onNewIntent(intent: Intent) {
@@ -55,14 +54,14 @@ function withAndroidConfig(config) {
55
54
  const androidManifest = config.modResults.manifest;
56
55
  const mainApplication = androidManifest.application[0];
57
56
 
58
- // 1. Configure MainActivity flags in Manifest
57
+ // 1. MainActivity flags
59
58
  const mainActivity = mainApplication.activity.find(a => a.$["android:name"] === ".MainActivity");
60
59
  if (mainActivity) {
61
60
  mainActivity.$["android:showWhenLocked"] = "true";
62
61
  mainActivity.$["android:turnScreenOn"] = "true";
63
62
  }
64
63
 
65
- // 2. Unified Permissions
64
+ // 2. Permissions
66
65
  const permissions = [
67
66
  'android.permission.USE_FULL_SCREEN_INTENT',
68
67
  'android.permission.VIBRATE',
@@ -70,7 +69,9 @@ function withAndroidConfig(config) {
70
69
  'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
71
70
  'android.permission.POST_NOTIFICATIONS',
72
71
  'android.permission.WAKE_LOCK',
72
+ 'android.permission.MANAGE_OWN_CALLS',
73
73
  'android.permission.DISABLE_KEYGUARD',
74
+ 'android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS',
74
75
  'android.permission.RECEIVE_BOOT_COMPLETED'
75
76
  ];
76
77
 
@@ -81,7 +82,7 @@ function withAndroidConfig(config) {
81
82
  }
82
83
  });
83
84
 
84
- // 3. AcceptCallActivity Registration
85
+ // 3. Activity/Service/Receiver registration (Cleaned up duplicates)
85
86
  mainApplication.activity = mainApplication.activity || [];
86
87
  if (!mainApplication.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
87
88
  mainApplication.activity.push({
@@ -95,48 +96,34 @@ function withAndroidConfig(config) {
95
96
  });
96
97
  }
97
98
 
98
- // 4. Service Setup
99
99
  mainApplication.service = mainApplication.service || [];
100
100
 
101
- const services = [
102
- {
103
- name: 'com.rnsnativecall.CallMessagingService',
104
- intentFilter: 'com.google.firebase.MESSAGING_EVENT'
105
- },
106
- {
107
- name: 'com.rnsnativecall.CallHeadlessTask'
108
- }
109
- ];
101
+ // Messaging Service
102
+ if (!mainApplication.service.some(s => s.$['android:name'] === 'com.rnsnativecall.CallMessagingService')) {
103
+ mainApplication.service.push({
104
+ $: { 'android:name': 'com.rnsnativecall.CallMessagingService', 'android:exported': 'false' },
105
+ 'intent-filter': [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
106
+ });
107
+ }
110
108
 
111
- services.forEach(svc => {
112
- if (!mainApplication.service.some(s => s.$['android:name'] === svc.name)) {
113
- const serviceEntry = {
114
- $: {
115
- 'android:name': svc.name,
116
- 'android:exported': 'false',
117
- 'android:foregroundServiceType': 'phoneCall'
118
- }
119
- };
120
- if (svc.intentFilter) {
121
- serviceEntry['intent-filter'] = [{ action: [{ $: { 'android:name': svc.intentFilter } }] }];
122
- }
123
- mainApplication.service.push(serviceEntry);
109
+ // Headless Task Service (Fixed for Android 14)
110
+ const headlessName = 'com.rnsnativecall.CallHeadlessTask';
111
+ const headlessIndex = mainApplication.service.findIndex(s => s.$['android:name'] === headlessName);
112
+ const headlessEntry = {
113
+ $: {
114
+ 'android:name': headlessName,
115
+ 'android:exported': 'false',
116
+ 'android:foregroundServiceType': 'phoneCall'
124
117
  }
125
- });
118
+ };
126
119
 
127
- // 5. Receiver Setup
128
- mainApplication.receiver = mainApplication.receiver || [];
129
- if (!mainApplication.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
130
- mainApplication.receiver.push({
131
- $: { 'android:name': 'com.rnsnativecall.CallActionReceiver', 'android:exported': 'false' }
132
- });
133
- }
120
+ if (headlessIndex > -1) mainApplication.service[headlessIndex] = headlessEntry;
121
+ else mainApplication.service.push(headlessEntry);
134
122
 
135
123
  return config;
136
124
  });
137
125
  }
138
126
 
139
- /** 3. IOS CONFIG **/
140
127
  function withIosConfig(config) {
141
128
  return withInfoPlist(config, (config) => {
142
129
  const infoPlist = config.modResults;
@@ -1,148 +0,0 @@
1
- ///withCallNativeConfig.js
2
- const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity } = require('@expo/config-plugins');
3
-
4
- /** 1. ANDROID MAIN ACTIVITY MODS **/
5
- function withMainActivityDataFix(config) {
6
- return withMainActivity(config, (config) => {
7
- let contents = config.modResults.contents;
8
-
9
- // Ensure necessary Imports
10
- if (!contents.includes('import android.content.Intent')) {
11
- contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
12
- }
13
- if (!contents.includes('import android.os.Bundle')) {
14
- contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.os.Bundle`);
15
- }
16
-
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 by a background task, push to back immediately
28
- if (intent.getBooleanExtra("background_wake", false)) {
29
- moveTaskToBack(true)
30
- }
31
- }
32
- `;
33
-
34
- // Insertion logic
35
- if (!contents.includes('override fun onNewIntent')) {
36
- contents = contents.replace(/class MainActivity : .*/, (match) => `${match}\n${onNewIntentCode}`);
37
- }
38
-
39
- if (!contents.includes('override fun onCreate')) {
40
- // Find the end of the class or after onNewIntent
41
- const lastBraceIndex = contents.lastIndexOf('}');
42
- contents = contents.slice(0, lastBraceIndex) + onCreateCode + contents.slice(lastBraceIndex);
43
- }
44
-
45
- config.modResults.contents = contents;
46
- return config;
47
- });
48
- }
49
-
50
- /** 2. ANDROID MANIFEST CONFIG **/
51
- function withAndroidConfig(config) {
52
- return withAndroidManifest(config, (config) => {
53
- const manifest = config.modResults.manifest;
54
- const application = manifest.application[0];
55
-
56
- // 1. Permissions (Updated for Android 14 compatibility)
57
- const permissions = [
58
- 'android.permission.USE_FULL_SCREEN_INTENT',
59
- 'android.permission.VIBRATE',
60
- 'android.permission.FOREGROUND_SERVICE',
61
- 'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
62
- 'android.permission.POST_NOTIFICATIONS',
63
- 'android.permission.WAKE_LOCK',
64
- 'android.permission.DISABLE_KEYGUARD',
65
- 'android.permission.RECEIVE_BOOT_COMPLETED'
66
- ];
67
-
68
- manifest['uses-permission'] = manifest['uses-permission'] || [];
69
- permissions.forEach((perm) => {
70
- if (!manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
71
- manifest['uses-permission'].push({ $: { 'android:name': perm } });
72
- }
73
- });
74
-
75
- // 2. Activity Setup (AcceptCallActivity)
76
- application.activity = application.activity || [];
77
- if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
78
- application.activity.push({
79
- $: {
80
- 'android:name': 'com.rnsnativecall.AcceptCallActivity',
81
- 'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
82
- 'android:excludeFromRecents': 'true',
83
- 'android:noHistory': 'true',
84
- 'android:exported': 'false',
85
- 'android:launchMode': 'singleInstance',
86
- 'android:showWhenLocked': 'true',
87
- 'android:turnScreenOn': 'true'
88
- }
89
- });
90
- }
91
-
92
- // 3. Service Registration (The Headless Enforcer)
93
- application.service = application.service || [];
94
-
95
- // Firebase Messaging Service
96
- if (!application.service.some(s => s.$['android:name'] === 'com.rnsnativecall.CallMessagingService')) {
97
- application.service.push({
98
- $: {
99
- 'android:name': 'com.rnsnativecall.CallMessagingService',
100
- 'android:exported': 'false'
101
- },
102
- 'intent-filter': [{
103
- action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }]
104
- }]
105
- });
106
- }
107
-
108
- // Headless Task Service
109
- if (!application.service.some(s => s.$['android:name'] === 'com.rnsnativecall.CallHeadlessTask')) {
110
- application.service.push({
111
- $: {
112
- 'android:name': 'com.rnsnativecall.CallHeadlessTask',
113
- 'android:exported': 'false' // Headless JS should be internal only
114
- }
115
- });
116
- }
117
-
118
- // 4. Action Receiver
119
- application.receiver = application.receiver || [];
120
- if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
121
- application.receiver.push({
122
- $: { 'android:name': 'com.rnsnativecall.CallActionReceiver', 'android:exported': 'false' }
123
- });
124
- }
125
-
126
- return config;
127
- });
128
- }
129
-
130
- /** 3. IOS CONFIG **/
131
- function withIosConfig(config) {
132
- return withInfoPlist(config, (config) => {
133
- const infoPlist = config.modResults;
134
- if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
135
- ['voip', 'audio', 'remote-notification'].forEach(mode => {
136
- if (!infoPlist.UIBackgroundModes.includes(mode)) infoPlist.UIBackgroundModes.push(mode);
137
- });
138
- return config;
139
- });
140
- }
141
-
142
- module.exports = (config) => {
143
- return withPlugins(config, [
144
- withAndroidConfig,
145
- withMainActivityDataFix,
146
- withIosConfig
147
- ]);
148
- };
@@ -1,42 +0,0 @@
1
- //withCallPermissions.js
2
- const { withPlugins, withAndroidManifest } = require('@expo/config-plugins');
3
-
4
-
5
- /** 2. ANDROID MANIFEST MOD **/
6
- function withCallPermissions(config) {
7
- return withAndroidManifest(config, async (config) => {
8
- const androidManifest = config.modResults.manifest;
9
-
10
- // 1. Add the Full Screen Intent Permission
11
- if (!androidManifest["uses-permission"]) {
12
- androidManifest["uses-permission"] = [];
13
- }
14
-
15
- const hasPermission = androidManifest["uses-permission"].some(
16
- (p) => p.$["android:name"] === "android.permission.USE_FULL_SCREEN_INTENT"
17
- );
18
-
19
- if (!hasPermission) {
20
- androidManifest["uses-permission"].push({
21
- $: { "android:name": "android.permission.USE_FULL_SCREEN_INTENT" },
22
- });
23
- }
24
-
25
- // 2. Ensure the Activity has the correct flags to appear over lockscreen
26
- const mainApplication = androidManifest.application[0];
27
- const mainActivity = mainApplication.activity.find(
28
- (a) => a.$["android:name"] === ".MainActivity"
29
- );
30
-
31
- if (mainActivity) {
32
- mainActivity.$["android:showOnLockScreen"] = "true";
33
- mainActivity.$["android:lockTaskMode"] = "if_whitelisted";
34
- }
35
-
36
- return config;
37
- });
38
- }
39
-
40
- module.exports = (config) => {
41
- return withPlugins(config, [withCallPermissions]);
42
- };