rns-nativecall 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ package com.rnsnativecall
2
+
3
+ import android.content.Intent
4
+ import com.facebook.react.HeadlessJsTaskService
5
+ import com.facebook.react.bridge.Arguments
6
+ import com.facebook.react.jstasks.HeadlessJsTaskConfig
7
+
8
+ class CallHeadlessTask : HeadlessJsTaskService() {
9
+ override fun getTaskConfig(intent: Intent): HeadlessJsTaskConfig? {
10
+ val extras = intent.extras
11
+ return if (extras != null) {
12
+ // INCREASE timeout to 30000 (30s) so it covers your 18s delay
13
+ HeadlessJsTaskConfig(
14
+ "ColdStartCallTask",
15
+ Arguments.fromBundle(extras),
16
+ 30000,
17
+ true // Allow in foreground
18
+ )
19
+ } else {
20
+ null
21
+ }
22
+ }
23
+ }
@@ -3,55 +3,103 @@ package com.rnsnativecall
3
3
  import android.app.ActivityManager
4
4
  import android.content.Context
5
5
  import android.content.Intent
6
+ import android.os.Bundle
6
7
  import android.os.Handler
7
8
  import android.os.Looper
9
+ import android.app.NotificationManager
10
+ import androidx.core.app.NotificationCompat
8
11
  import com.google.firebase.messaging.FirebaseMessagingService
9
12
  import com.google.firebase.messaging.RemoteMessage
13
+ import com.facebook.react.HeadlessJsTaskService
10
14
 
11
15
  class CallMessagingService : FirebaseMessagingService() {
12
16
 
17
+ // This companion object allows the Cancel logic to find the active timer
18
+ companion object {
19
+ private val handler = Handler(Looper.getMainLooper())
20
+ private val pendingNotifications = mutableMapOf<String, Runnable>()
21
+ }
22
+
13
23
  override fun onMessageReceived(remoteMessage: RemoteMessage) {
14
24
  val data = remoteMessage.data
15
25
  val context = applicationContext
16
26
 
27
+ // Extract variables for logic
28
+ val uuid = data["callUuid"] ?: return
29
+ val type = data["type"] ?: ""
30
+
31
+ // CASE 1: CALLER HUNG UP / CANCELLED
32
+ 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
+ }
47
+
48
+ // CASE 2: NEW INCOMING CALL
17
49
  if (isAppInForeground(context)) {
50
+ // App is visible: Send event to active JS bridge immediately
18
51
  CallModule.sendEventToJS("onCallReceived", data)
19
52
  } else {
20
- // 1. START WAKING THE APP IMMEDIATELY
21
- // We launch the "AcceptCallActivity" with a special flag
22
- // OR just trigger the Bridge via an Intent.
23
- wakeUpReactContext(context, data)
24
-
25
- // 2. Start the 18-second "Safety" timer
26
- val handler = Handler(Looper.getMainLooper())
27
- handler.postDelayed({
28
- // Re-check: If the bridge woke up and the user navigated in-app,
29
- // isAppInForeground will now be true.
30
- if (!isAppInForeground(context)) {
31
- NativeCallManager.handleIncomingPush(context, data)
32
- }
33
- }, 18000)
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)
61
+
62
+ // You SHOULD see "Android Bundled..." now because the Activity is starting
63
+
64
+ val showNotificationRunnable = Runnable {
65
+ if (!isAppInForeground(context)) {
66
+ NativeCallManager.handleIncomingPush(context, data)
34
67
  }
35
68
  }
69
+ handler.postDelayed(showNotificationRunnable, 18000)
70
+ }
71
+ }
36
72
 
37
- private fun wakeUpReactContext(context: Context, data: Map<String, String>) {
38
- try {
39
- // We use the Launch Intent to trigger the splash/main activity boot sequence
40
- // but we don't bring it to the front yet (it stays in background process)
41
- val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
42
- launchIntent?.apply {
43
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
44
- // Add a hidden flag so your Splash screen knows it's a "silent boot"
45
- putExtra("silent_wake", true)
46
- data.forEach { (key, value) -> putExtra(key, value) }
47
- }
48
-
49
- // This starts the process and the React Native Bridge
50
- // Note: On Android 10+, this won't show the UI, but it WILL start the process.
51
- context.startActivity(launchIntent)
52
- } catch (e: Exception) {
53
- // Log error
54
- }
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
+ private fun showMissedCallNotification(context: Context, data: Map<String, String>, uuid: String) {
84
+ val name = data["name"] ?: "Unknown"
85
+ val callType = data["callType"] ?: "video"
86
+ val channelId = "missed_calls" // Ensure this channel is created in your MainApplication or CallManager
87
+
88
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
89
+
90
+ // Use ic_launcher as fallback if ic_missed_call isn't found
91
+ var iconResId = context.resources.getIdentifier("ic_missed_call", "drawable", context.packageName)
92
+ if (iconResId == 0) iconResId = android.R.drawable.sym_call_missed
93
+
94
+ val builder = NotificationCompat.Builder(context, channelId)
95
+ .setSmallIcon(iconResId)
96
+ .setContentTitle("Missed $callType call")
97
+ .setContentText("You missed a call from $name")
98
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
99
+ .setAutoCancel(true)
100
+ .setCategory(NotificationCompat.CATEGORY_MISS)
101
+
102
+ notificationManager.notify(uuid.hashCode(), builder.build())
55
103
  }
56
104
 
57
105
  private fun isAppInForeground(context: Context): Boolean {
@@ -28,7 +28,6 @@ object NativeCallManager {
28
28
  } else {
29
29
  PendingIntent.FLAG_UPDATE_CURRENT
30
30
  }
31
-
32
31
  // 1. IMPROVED INTENT: Direct to AcceptCallActivity
33
32
  // This is better than a dummy intent because it allows the OS to
34
33
  // launch your "Accept" logic immediately if the phone is locked.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
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",
@@ -5,10 +5,15 @@ function withMainActivityDataFix(config) {
5
5
  return withMainActivity(config, (config) => {
6
6
  let contents = config.modResults.contents;
7
7
 
8
+ // 1. Ensure necessary Imports are present
8
9
  if (!contents.includes('import android.content.Intent')) {
9
10
  contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
10
11
  }
12
+ if (!contents.includes('import android.os.Bundle')) {
13
+ contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.os.Bundle`);
14
+ }
11
15
 
16
+ // 2. Define the code blocks
12
17
  const onNewIntentCode = `
13
18
  override fun onNewIntent(intent: Intent) {
14
19
  super.onNewIntent(intent)
@@ -16,11 +21,28 @@ function withMainActivityDataFix(config) {
16
21
  }
17
22
  `;
18
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
19
35
  if (!contents.includes('override fun onNewIntent')) {
20
36
  const lastBraceIndex = contents.lastIndexOf('}');
21
37
  contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
22
38
  }
23
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
+
24
46
  config.modResults.contents = contents;
25
47
  return config;
26
48
  });
@@ -50,37 +72,29 @@ function withAndroidConfig(config) {
50
72
  }
51
73
  });
52
74
 
53
- // 2. Activity Cleanup & Setup
75
+ // 2. Activity Setup
54
76
  application.activity = application.activity || [];
55
77
 
56
- // Remove old/redundant activities
57
- application.activity = application.activity.filter(a =>
58
- a.$['android:name'] !== 'com.rnsnativecall.IncomingCallActivity' &&
59
- a.$['android:name'] !== 'com.rnsnativecall.AcceptCallActivity'
60
- );
61
-
62
- // Add the specific AcceptCallActivity (Trampoline)
63
- application.activity.push({
64
- $: {
65
- 'android:name': 'com.rnsnativecall.AcceptCallActivity',
66
- 'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
67
- 'android:excludeFromRecents': 'true',
68
- 'android:noHistory': 'true',
69
- 'android:exported': 'false',
70
- 'android:launchMode': 'singleInstance',
71
- 'android:showWhenLocked': 'true',
72
- 'android:turnScreenOn': 'true'
73
- }
74
- });
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
+ }
75
93
 
76
- // 3. Service Cleanup & Setup (Firebase only)
94
+ // 3. Service Setup (Firebase + Headless Task)
77
95
  application.service = application.service || [];
78
96
 
79
- // Explicitly remove Telecom Service
80
- application.service = application.service.filter(
81
- s => s.$['android:name'] !== 'com.rnsnativecall.MyConnectionService'
82
- );
83
-
97
+ // Add Firebase Messaging Service
84
98
  const firebaseServiceName = 'com.rnsnativecall.CallMessagingService';
85
99
  if (!application.service.some(s => s.$['android:name'] === firebaseServiceName)) {
86
100
  application.service.push({
@@ -92,6 +106,17 @@ function withAndroidConfig(config) {
92
106
  });
93
107
  }
94
108
 
109
+ // Add Headless JS Task Service
110
+ const headlessServiceName = 'com.rnsnativecall.CallHeadlessTask';
111
+ if (!application.service.some(s => s.$['android:name'] === headlessServiceName)) {
112
+ application.service.push({
113
+ $: {
114
+ 'android:name': headlessServiceName,
115
+ 'android:exported': 'false'
116
+ }
117
+ });
118
+ }
119
+
95
120
  // 4. Receiver Setup
96
121
  application.receiver = application.receiver || [];
97
122
  const receiverName = 'com.rnsnativecall.CallActionReceiver';
@@ -101,6 +126,8 @@ function withAndroidConfig(config) {
101
126
  });
102
127
  }
103
128
 
129
+
130
+
104
131
  return config;
105
132
  });
106
133
  }