rns-nativecall 0.7.1 → 0.7.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.
@@ -13,7 +13,6 @@ class AcceptCallActivity : Activity() {
13
13
  override fun onCreate(savedInstanceState: Bundle?) {
14
14
  super.onCreate(savedInstanceState)
15
15
 
16
- // Ensure we show over the lockscreen
17
16
  window.addFlags(
18
17
  WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
19
18
  WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
@@ -23,6 +22,14 @@ class AcceptCallActivity : Activity() {
23
22
  val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
24
23
  keyguardManager.requestDismissKeyguard(this, null)
25
24
 
25
+ // Check: Did the user actually press "Answer" or did the system auto-launch?
26
+ // If the action is null or doesn't match your Answer action,
27
+ // it means the system is just "preparing" the activity.
28
+ if (intent.action?.startsWith("ACTION_ANSWER") != true) {
29
+ // If it's just an auto-launch, we don't fire the JS event!
30
+ // We can either finish() or show a tiny "Swipe to Answer" UI.
31
+ return
32
+ }
26
33
  processCallIntent(intent)
27
34
  }
28
35
 
@@ -35,30 +42,36 @@ class AcceptCallActivity : Activity() {
35
42
  val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
36
43
  uuid?.let { notificationManager.cancel(it.hashCode()) }
37
44
 
38
- val dataMap = mutableMapOf<String, String>()
39
- extras?.keySet()?.forEach { key ->
40
- extras.get(key)?.let { dataMap[key] = it.toString() }
41
- }
42
-
43
- // 1. Set the data for JS (Cold start support)
44
- CallModule.setPendingCallData("onCallAccepted_pending", dataMap)
45
-
46
- // 2. Fire event immediately if JS is alive
47
- if (CallModule.isReady()) {
48
- CallModule.sendEventToJS("onCallAccepted", dataMap)
49
- }
50
-
51
- // 3. Bring the Main App to the front
45
+ // WE STOP SENDING THE JS EVENT HERE.
46
+ // Instead, we pass the intent to MainActivity with a specific ACTION.
52
47
  openMainApp(extras)
53
48
  finish()
54
49
  }
55
50
 
56
51
  private fun openMainApp(extras: Bundle?) {
57
- val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
58
- launchIntent?.apply {
59
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
60
- putExtras(extras ?: Bundle())
61
- startActivity(this)
52
+ try {
53
+ // Get the actual MainActivity class name (e.g., com.yourapp.MainActivity)
54
+ val mainActivityClassName = "${packageName}.MainActivity"
55
+
56
+ val intent = Intent().apply {
57
+ setClassName(packageName, mainActivityClassName)
58
+ action = "com.rnsnativecall.ACTION_ANSWER"
59
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
60
+
61
+ // Ensure extras are carried over
62
+ extras?.let { putExtras(it) }
63
+ }
64
+
65
+ startActivity(intent)
66
+ } catch (e: Exception) {
67
+ // Fallback: If explicit mapping fails, try the launch intent but force the action
68
+ val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
69
+ launchIntent?.apply {
70
+ action = "com.rnsnativecall.ACTION_ANSWER"
71
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
72
+ extras?.let { putExtras(it) }
73
+ startActivity(this)
74
+ }
62
75
  }
63
76
  }
64
77
  }
@@ -24,28 +24,18 @@ class CallMessagingService : FirebaseMessagingService() {
24
24
 
25
25
  val uuid = data["callUuid"] ?: return
26
26
  val type = data["type"] ?: ""
27
-
27
+
28
28
  if (type == "CANCEL") {
29
29
  NativeCallManager.stopRingtone()
30
- CallState.markCanceled(uuid)
30
+ // Pass context here to persist the cancellation
31
+ CallState.markCanceled(uuid, context)
31
32
 
32
- try {
33
- // Direct disk-level cleanup of AsyncStorage
34
- val prefs = context.getSharedPreferences("RKStorage", Context.MODE_PRIVATE)
35
- prefs.edit()
36
- .remove("pending_call_uuid")
37
- .remove("offer_$uuid")
38
- .apply()
39
-
40
- // Kill the 'Connecting...' service if it's visible
41
- context.stopService(Intent(context, CallForegroundService::class.java))
42
- } catch (e: Exception) {
43
- e.printStackTrace()
44
- }
45
-
46
33
  if (CallState.getCurrent() == uuid) {
47
- CallState.clear(uuid)
34
+ CallState.clear(uuid, context)
48
35
  }
36
+
37
+ // Dismiss the "Connecting..." or "Incoming Call" UI
38
+ NativeCallManager.dismissIncomingCall(context, uuid)
49
39
  showMissedCallNotification(context, data, uuid)
50
40
  return
51
41
  }
@@ -50,6 +50,7 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
50
50
  promise.reject("CALL_ERROR", e.message)
51
51
  }
52
52
  }
53
+
53
54
 
54
55
  /**
55
56
  * Combined Validity Check:
@@ -57,13 +58,14 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
57
58
  */
58
59
  @ReactMethod
59
60
  fun checkCallValidity(uuid: String, promise: Promise) {
60
- val isValid = CallState.shouldProceed(uuid)
61
- val isCanceled = CallState.isCanceled(uuid)
62
-
63
- val map = Arguments.createMap().apply {
64
- putBoolean("isValid", isValid)
65
- putBoolean("isCanceled", isCanceled)
66
- }
61
+ // Pass context so it can check the persistent disk storage
62
+ val isValid = CallState.shouldProceed(uuid, reactApplicationContext)
63
+ val isCanceled = CallState.isCanceled(uuid, reactApplicationContext)
64
+
65
+ val map = Arguments.createMap().apply {
66
+ putBoolean("isValid", isValid)
67
+ putBoolean("isCanceled", isCanceled)
68
+ }
67
69
  promise.resolve(map)
68
70
  }
69
71
 
@@ -1,9 +1,17 @@
1
1
  package com.rnsnativecall
2
2
 
3
+ import android.content.Context
4
+ import android.content.SharedPreferences
5
+
3
6
  object CallState {
4
7
  @Volatile private var currentUuid: String? = null
5
8
  @Volatile private var canceledUuids = mutableSetOf<String>()
6
9
 
10
+ // Helper to access persistent storage
11
+ private fun getPrefs(context: Context): SharedPreferences {
12
+ return context.getSharedPreferences("RNS_CALL_STATE_INTERNAL", Context.MODE_PRIVATE)
13
+ }
14
+
7
15
  @Synchronized
8
16
  fun isBusy(): Boolean = currentUuid != null
9
17
 
@@ -19,37 +27,56 @@ object CallState {
19
27
  fun getCurrent(): String? = currentUuid
20
28
 
21
29
  @Synchronized
22
- fun markCanceled(uuid: String) {
30
+ fun markCanceled(uuid: String, context: Context? = null) {
23
31
  canceledUuids.add(uuid)
24
32
  if (currentUuid == uuid) {
25
33
  currentUuid = null
26
34
  }
35
+ // PERSISTENCE: Save to disk so Splash screen can see it after app restart
36
+ context?.let {
37
+ getPrefs(it).edit().putBoolean("canceled_$uuid", true).apply()
38
+ }
27
39
  }
28
40
 
29
- // ✅ Fixes the "Unresolved reference 'isCanceled'" error
30
41
  @Synchronized
31
- fun isCanceled(uuid: String?): Boolean {
42
+ fun isCanceled(uuid: String?, context: Context? = null): Boolean {
32
43
  if (uuid == null) return false
33
- return canceledUuids.contains(uuid)
44
+ // Check memory first
45
+ if (canceledUuids.contains(uuid)) return true
46
+
47
+ // PERSISTENCE: Check disk if memory was wiped (App was killed)
48
+ return context?.let {
49
+ getPrefs(it).getBoolean("canceled_$uuid", false)
50
+ } ?: false
34
51
  }
35
52
 
36
53
  @Synchronized
37
- fun clear(uuid: String?) {
54
+ fun clear(uuid: String?, context: Context? = null) {
38
55
  if (uuid == null) return
39
56
  if (currentUuid == uuid) currentUuid = null
40
57
  canceledUuids.remove(uuid)
58
+
59
+ // PERSISTENCE: Cleanup disk entry
60
+ context?.let {
61
+ getPrefs(it).edit().remove("canceled_$uuid").apply()
62
+ }
41
63
  }
42
64
 
43
65
  @Synchronized
44
- fun shouldProceed(uuid: String?): Boolean {
66
+ fun shouldProceed(uuid: String?, context: Context? = null): Boolean {
45
67
  if (uuid == null) return false
46
- return (currentUuid == uuid) && !canceledUuids.contains(uuid)
68
+
69
+ // A call should proceed if it is the current UUID AND it hasn't been canceled
70
+ // We use the context-aware isCanceled to check the disk if necessary
71
+ val canceled = isCanceled(uuid, context)
72
+ return (currentUuid == uuid || currentUuid == null) && !canceled
47
73
  }
48
74
 
49
75
  @Synchronized
50
76
  fun clearAll() {
51
77
  currentUuid = null
52
78
  canceledUuids.clear()
79
+ // Note: clearAll usually doesn't have context,
80
+ // specific UUIDs are usually cleared via the clear(uuid, context) method
53
81
  }
54
- }
55
-
82
+ }
@@ -11,92 +11,158 @@ import android.media.Ringtone
11
11
  import android.media.RingtoneManager
12
12
  import android.graphics.Color
13
13
 
14
+ import androidx.core.app.Person
15
+ import androidx.core.app.NotificationCompat
16
+
14
17
  object NativeCallManager {
15
18
 
16
19
  private var ringtone: Ringtone? = null
17
20
  const val channelId = "CALL_CHANNEL_ID"
18
21
 
19
- fun handleIncomingPush(context: Context, data: Map<String, String>) {
20
- val uuid = data["callUuid"] ?: return
21
- stopRingtone()
22
-
23
- val name = data["name"] ?: "Incoming Call"
24
- val callType = data["callType"] ?: "audio"
25
- val notificationId = uuid.hashCode()
26
-
27
- val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
28
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
29
- } else {
30
- PendingIntent.FLAG_UPDATE_CURRENT
31
- }
32
-
33
- val noOpIntent = PendingIntent.getActivity(
34
- context,
35
- notificationId + 1,
36
- Intent(),
37
- pendingFlags
38
- )
39
-
40
- val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
41
- this.action = "ACTION_SHOW_UI_$uuid"
42
- data.forEach { (key, value) -> this.putExtra(key, value) }
43
- this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
44
- }
22
+ // fun handleIncomingPush(context: Context, data: Map<String, String>) {
23
+ // val uuid = data["callUuid"] ?: return
24
+ // stopRingtone()
25
+
26
+ // val name = data["name"] ?: "Incoming Call"
27
+ // val callType = data["callType"] ?: "audio"
28
+ // val notificationId = uuid.hashCode()
29
+
30
+ // val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
31
+ // PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
32
+ // } else {
33
+ // PendingIntent.FLAG_UPDATE_CURRENT
34
+ // }
35
+
36
+ // val noOpIntent = PendingIntent.getActivity(
37
+ // context,
38
+ // notificationId + 1,
39
+ // Intent(),
40
+ // pendingFlags
41
+ // )
42
+
43
+ // val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
44
+ // this.action = "ACTION_SHOW_UI_$uuid"
45
+ // data.forEach { (key, value) -> this.putExtra(key, value) }
46
+ // this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
47
+ // }
48
+
49
+ // val fullScreenPendingIntent = PendingIntent.getActivity(
50
+ // context,
51
+ // notificationId,
52
+ // intentToActivity,
53
+ // pendingFlags
54
+ // )
55
+
56
+ // val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
57
+ // this.action = "ACTION_REJECT_$uuid"
58
+ // this.putExtra("EXTRA_CALL_UUID", uuid)
59
+ // data.forEach { (key, value) -> this.putExtra(key, value) }
60
+ // }
61
+
62
+ // val rejectPendingIntent = PendingIntent.getBroadcast(
63
+ // context,
64
+ // notificationId,
65
+ // rejectIntent,
66
+ // pendingFlags
67
+ // )
68
+
69
+ // val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
70
+
71
+ // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
72
+ // val channel = NotificationChannel(
73
+ // channelId,
74
+ // "Incoming Calls",
75
+ // NotificationManager.IMPORTANCE_HIGH // NotificationManager.IMPORTANCE_HIGH
76
+ // ).apply {
77
+ // enableVibration(true)
78
+ // vibrationPattern = longArrayOf(0, 500, 500, 500)
79
+ // lightColor = Color.GREEN
80
+ // setBypassDnd(true)
81
+ // lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
82
+ // setSound(null, null)
83
+ // }
84
+ // notificationManager.createNotificationChannel(channel)
85
+ // }
86
+
87
+ // val builder = NotificationCompat.Builder(context, channelId)
88
+ // .setSmallIcon(context.applicationInfo.icon)
89
+ // .setContentTitle("Incoming $callType call")
90
+ // .setContentText(name)
91
+ // .setPriority(NotificationCompat.PRIORITY_MAX) // PRIORITY_HIGH
92
+ // .setCategory(NotificationCompat.CATEGORY_CALL) // CATEGORY_CALL
93
+ // .setOngoing(true)
94
+ // .setAutoCancel(false)
95
+ // .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
96
+ // .setFullScreenIntent(fullScreenPendingIntent, true)
97
+ // .setContentIntent(noOpIntent)
98
+ // .addAction(0, "Answer", fullScreenPendingIntent)
99
+ // .addAction(0, "Decline", rejectPendingIntent)
100
+
101
+ // notificationManager.notify(notificationId, builder.build())
102
+
103
+ // try {
104
+ // val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
105
+ // ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
106
+ // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
107
+ // ringtone?.isLooping = true
108
+ // }
109
+ // ringtone?.play()
110
+ // } catch (e: Exception) {
111
+ // e.printStackTrace()
112
+ // }
113
+ // }
114
+
115
+
116
+ fun handleIncomingPush(context: Context, data: Map<String, String>) {
117
+ val uuid = data["callUuid"] ?: return
118
+ stopRingtone()
119
+
120
+ val name = data["name"] ?: "Incoming Call"
121
+ val callType = data["callType"] ?: "audio"
122
+ val notificationId = uuid.hashCode()
45
123
 
46
- val fullScreenPendingIntent = PendingIntent.getActivity(
47
- context,
48
- notificationId,
49
- intentToActivity,
50
- pendingFlags
51
- )
124
+ // 1. Create the Person object (Required for CallStyle)
125
+ val caller = Person.Builder()
126
+ .setName(name)
127
+ .setImportant(true)
128
+ .build()
129
+
130
+ // 2. Intents (Keep your existing intent logic)
131
+ val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
132
+ this.action = "ACTION_ANSWER_$uuid"
133
+ data.forEach { (key, value) -> this.putExtra(key, value) }
134
+ this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
135
+ }
52
136
 
53
- val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
54
- this.action = "ACTION_REJECT_$uuid"
55
- this.putExtra("EXTRA_CALL_UUID", uuid)
56
- data.forEach { (key, value) -> this.putExtra(key, value) }
57
- }
137
+ val fullScreenPendingIntent = PendingIntent.getActivity(
138
+ context, notificationId, intentToActivity,
139
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
140
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
141
+ else PendingIntent.FLAG_UPDATE_CURRENT
142
+ )
58
143
 
59
- val rejectPendingIntent = PendingIntent.getBroadcast(
60
- context,
61
- notificationId,
62
- rejectIntent,
63
- pendingFlags
144
+ // 3. Build the Notification with CallStyle
145
+ val builder = NotificationCompat.Builder(context, channelId)
146
+ .setSmallIcon(context.applicationInfo.icon)
147
+ .setPriority(NotificationCompat.PRIORITY_MAX)
148
+ .setCategory(NotificationCompat.CATEGORY_CALL)
149
+ .setOngoing(true)
150
+ .setAutoCancel(false)
151
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
152
+ // This makes it show on the lock screen WITHOUT auto-launching the activity
153
+ .setFullScreenIntent(fullScreenPendingIntent, true)
154
+ .setStyle(
155
+ NotificationCompat.CallStyle.forIncomingCall(
156
+ caller,
157
+ rejectPendingIntent, // Use your existing rejectIntent
158
+ fullScreenPendingIntent
159
+ )
64
160
  )
65
161
 
66
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
67
-
68
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
69
- val channel = NotificationChannel(
70
- channelId,
71
- "Incoming Calls",
72
- NotificationManager.IMPORTANCE_HIGH // NotificationManager.IMPORTANCE_HIGH
73
- ).apply {
74
- enableVibration(true)
75
- vibrationPattern = longArrayOf(0, 500, 500, 500)
76
- lightColor = Color.GREEN
77
- setBypassDnd(true)
78
- lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
79
- setSound(null, null)
80
- }
81
- notificationManager.createNotificationChannel(channel)
82
- }
83
-
84
- val builder = NotificationCompat.Builder(context, channelId)
85
- .setSmallIcon(context.applicationInfo.icon)
86
- .setContentTitle("Incoming $callType call")
87
- .setContentText(name)
88
- .setPriority(NotificationCompat.PRIORITY_MAX) // PRIORITY_HIGH
89
- .setCategory(NotificationCompat.CATEGORY_CALL) // CATEGORY_CALL
90
- .setOngoing(true)
91
- .setAutoCancel(false)
92
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
93
- .setFullScreenIntent(fullScreenPendingIntent, true)
94
- .setContentIntent(noOpIntent)
95
- .addAction(0, "Answer", fullScreenPendingIntent)
96
- .addAction(0, "Decline", rejectPendingIntent)
97
-
98
- notificationManager.notify(notificationId, builder.build())
99
-
162
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
163
+ // ... ensure channel is created ...
164
+ notificationManager.notify(notificationId, builder.build())
165
+
100
166
  try {
101
167
  val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
102
168
  ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
@@ -107,7 +173,7 @@ object NativeCallManager {
107
173
  } catch (e: Exception) {
108
174
  e.printStackTrace()
109
175
  }
110
- }
176
+ }
111
177
 
112
178
  fun stopRingtone() {
113
179
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rns-nativecall",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "High-performance React Native module for handling native VoIP call UI on Android and iOS.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",