@stream-io/react-native-callingx 0.1.0-beta.7 → 0.1.1-beta.0

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.
Files changed (45) hide show
  1. package/android/build.gradle +7 -1
  2. package/android/src/main/AndroidManifest.xml +31 -1
  3. package/android/src/main/java/io/getstream/rn/callingx/CallEventBroadcastReceiver.kt +17 -0
  4. package/android/src/main/java/io/getstream/rn/callingx/CallRegistrationStore.kt +176 -0
  5. package/android/src/main/java/io/getstream/rn/callingx/CallService.kt +302 -80
  6. package/android/src/main/java/io/getstream/rn/callingx/CallingxModuleImpl.kt +176 -191
  7. package/android/src/main/java/io/getstream/rn/callingx/StreamMessagingService.kt +48 -0
  8. package/android/src/main/java/io/getstream/rn/callingx/model/Call.kt +1 -0
  9. package/android/src/main/java/io/getstream/rn/callingx/notifications/CallNotificationManager.kt +196 -46
  10. package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationIntentFactory.kt +14 -8
  11. package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverActivity.kt +12 -1
  12. package/android/src/main/java/io/getstream/rn/callingx/notifications/NotificationReceiverService.kt +7 -0
  13. package/android/src/main/java/io/getstream/rn/callingx/repo/CallRepository.kt +38 -19
  14. package/android/src/main/java/io/getstream/rn/callingx/repo/LegacyCallRepository.kt +64 -55
  15. package/android/src/main/java/io/getstream/rn/callingx/repo/TelecomCallRepository.kt +241 -195
  16. package/android/src/main/java/io/getstream/rn/callingx/utils/CallEventBus.kt +61 -0
  17. package/android/src/main/java/io/getstream/rn/callingx/utils/SettingsStore.kt +51 -0
  18. package/android/src/newarch/java/io/getstream/rn/callingx/CallingxModule.kt +12 -3
  19. package/android/src/oldarch/java/io/getstream/rn/callingx/CallingxModule.kt +13 -3
  20. package/dist/module/CallingxModule.js +13 -10
  21. package/dist/module/CallingxModule.js.map +1 -1
  22. package/dist/module/spec/NativeCallingx.js.map +1 -1
  23. package/dist/module/utils/constants.js +24 -13
  24. package/dist/module/utils/constants.js.map +1 -1
  25. package/dist/typescript/src/CallingxModule.d.ts +3 -0
  26. package/dist/typescript/src/CallingxModule.d.ts.map +1 -1
  27. package/dist/typescript/src/spec/NativeCallingx.d.ts +7 -1
  28. package/dist/typescript/src/spec/NativeCallingx.d.ts.map +1 -1
  29. package/dist/typescript/src/types.d.ts +31 -0
  30. package/dist/typescript/src/types.d.ts.map +1 -1
  31. package/dist/typescript/src/utils/constants.d.ts +1 -1
  32. package/dist/typescript/src/utils/constants.d.ts.map +1 -1
  33. package/ios/AudioSessionManager.swift +2 -2
  34. package/ios/Callingx.mm +41 -17
  35. package/ios/CallingxImpl.swift +213 -83
  36. package/ios/Settings.swift +2 -2
  37. package/ios/UUIDStorage.swift +10 -10
  38. package/ios/VoipNotificationsManager.swift +8 -8
  39. package/package.json +4 -2
  40. package/src/CallingxModule.ts +14 -10
  41. package/src/spec/NativeCallingx.ts +10 -3
  42. package/src/types.ts +34 -0
  43. package/src/utils/constants.ts +23 -9
  44. /package/android/src/main/java/io/getstream/rn/callingx/{ResourceUtils.kt → utils/ResourceUtils.kt} +0 -0
  45. /package/android/src/main/java/io/getstream/rn/callingx/{Utils.kt → utils/Utils.kt} +0 -0
@@ -8,9 +8,7 @@ import android.content.IntentFilter
8
8
  import android.content.ServiceConnection
9
9
  import android.os.Build
10
10
  import android.os.Bundle
11
- import android.os.Handler
12
11
  import android.os.IBinder
13
- import android.os.Looper
14
12
  import android.telecom.DisconnectCause
15
13
  import android.util.Log
16
14
  import androidx.core.content.ContextCompat
@@ -27,12 +25,12 @@ import com.facebook.react.modules.core.DeviceEventManagerModule
27
25
  import io.getstream.rn.callingx.model.CallAction
28
26
  import io.getstream.rn.callingx.notifications.NotificationChannelsManager
29
27
  import io.getstream.rn.callingx.notifications.NotificationsConfig
30
- import java.util.concurrent.ConcurrentHashMap
28
+ import io.getstream.rn.callingx.utils.SettingsStore
31
29
 
32
30
  class CallingxModuleImpl(
33
31
  private val reactApplicationContext: ReactApplicationContext,
34
32
  private val eventEmitter: CallingxEventEmitterAdapter
35
- ) {
33
+ ) : CallEventBus.Listener {
36
34
 
37
35
  companion object {
38
36
  const val TAG = "[Callingx] CallingxModule"
@@ -45,19 +43,20 @@ class CallingxModuleImpl(
45
43
  const val EXTRA_AUDIO_ENDPOINT = "audio_endpoint"
46
44
  const val EXTRA_SOURCE = "source"
47
45
 
48
- const val CALL_REGISTERED_ACTION = "call_registered"
49
- const val CALL_REGISTERED_INCOMING_ACTION = "call_registered_incoming"
50
- const val CALL_ANSWERED_ACTION = "call_answered"
51
- // const val CALL_DISCONNECTED_ACTION = "call_disconnected"
52
- const val CALL_INACTIVE_ACTION = "call_inactive"
53
- const val CALL_ACTIVE_ACTION = "call_active"
54
- const val CALL_MUTED_ACTION = "call_muted"
55
- const val CALL_ENDPOINT_CHANGED_ACTION = "call_endpoint_changed"
56
- const val CALL_END_ACTION = "call_end"
57
- const val CALL_REGISTRATION_FAILED_ACTION = "call_registration_failed"
46
+ // Action names must match intent-filter entries in AndroidManifest.xml
47
+ const val CALL_REGISTERED_ACTION = "io.getstream.CALL_REGISTERED"
48
+ const val CALL_REGISTERED_INCOMING_ACTION = "io.getstream.CALL_REGISTERED_INCOMING"
49
+ const val CALL_ANSWERED_ACTION = "io.getstream.CALL_ANSWERED"
50
+ const val CALL_INACTIVE_ACTION = "io.getstream.CALL_INACTIVE"
51
+ const val CALL_ACTIVE_ACTION = "io.getstream.CALL_ACTIVE"
52
+ const val CALL_MUTED_ACTION = "io.getstream.CALL_MUTED"
53
+ const val CALL_ENDPOINT_CHANGED_ACTION = "io.getstream.CALL_ENDPOINT_CHANGED"
54
+ const val CALL_END_ACTION = "io.getstream.CALL_END"
55
+ const val CALL_REGISTRATION_FAILED_ACTION = "io.getstream.CALL_REGISTRATION_FAILED"
56
+ const val CALL_OPTIMISTIC_ACCEPT_ACTION = "io.getstream.ACCEPT_CALL_OPTIMISTIC"
58
57
  // Background task name
59
58
  const val HEADLESS_TASK_NAME = "HandleCallBackgroundState"
60
- const val SERVICE_READY_ACTION = "service_ready"
59
+ const val SERVICE_READY_ACTION = "io.getstream.SERVICE_READY"
61
60
  }
62
61
 
63
62
  private enum class BindingState {
@@ -74,18 +73,8 @@ class CallingxModuleImpl(
74
73
  private var canSendEvents = false
75
74
  private var isHeadlessTaskRegistered = false
76
75
 
77
- // Synchronous call tracking set, updated before async service start to mirror iOS semantics.
78
- // This ensures isCallTracked() returns true immediately after displayIncomingCall/startCall.
79
- private val trackedCallIds = ConcurrentHashMap.newKeySet<String>()
80
-
81
- // Per-callId pending promises for displayIncomingCall awaiting CALL_REGISTERED_INCOMING_ACTION
82
- private val pendingDisplayPromises = mutableMapOf<String, Promise>()
83
- private val pendingTimeouts = mutableMapOf<String, Runnable>()
84
- private val mainHandler = Handler(Looper.getMainLooper())
85
- private val displayTimeoutMs = 10_000L // 10 second safety timeout
86
-
87
76
  private val notificationChannelsManager = NotificationChannelsManager(reactApplicationContext)
88
- private val callEventBroadcastReceiver = CallEventBroadcastReceiver()
77
+ private val serviceReadyBroadcastReceiver = ServiceReadyBroadcastReceiver()
89
78
  private val appStateListener =
90
79
  object : LifecycleEventListener {
91
80
  override fun onHostResume() {}
@@ -100,15 +89,20 @@ class CallingxModuleImpl(
100
89
  }
101
90
 
102
91
  init {
92
+ CallEventBus.subscribe(this)
93
+
103
94
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
104
95
  reactApplicationContext.registerReceiver(
105
- callEventBroadcastReceiver,
106
- getReceiverFilter(),
96
+ serviceReadyBroadcastReceiver,
97
+ getServiceReadyReceiverFilter(),
107
98
  Context.RECEIVER_NOT_EXPORTED
108
99
  )
109
100
  } else {
110
101
  @Suppress("UnspecifiedRegisterReceiverFlag")
111
- reactApplicationContext.registerReceiver(callEventBroadcastReceiver, getReceiverFilter())
102
+ reactApplicationContext.registerReceiver(
103
+ serviceReadyBroadcastReceiver,
104
+ getServiceReadyReceiverFilter()
105
+ )
112
106
  }
113
107
  }
114
108
 
@@ -123,21 +117,24 @@ class CallingxModuleImpl(
123
117
  fun invalidate() {
124
118
  debugLog(TAG, "[module] invalidate: Invalidating module")
125
119
 
126
- // Clean up pending display promises to prevent leaks
127
- synchronized(pendingDisplayPromises) {
128
- pendingTimeouts.values.forEach { mainHandler.removeCallbacks(it) }
129
- pendingTimeouts.clear()
130
- pendingDisplayPromises.clear()
131
- }
132
-
133
- trackedCallIds.clear()
120
+ CallRegistrationStore.clearAll()
134
121
  unbindServiceSafely()
135
122
 
123
+ CallEventBus.unsubscribe(this)
124
+
136
125
  reactApplicationContext.removeLifecycleEventListener(appStateListener)
137
- reactApplicationContext.unregisterReceiver(callEventBroadcastReceiver)
126
+ reactApplicationContext.unregisterReceiver(serviceReadyBroadcastReceiver)
138
127
  isModuleInitialized = false
139
128
  }
140
129
 
130
+ fun setShouldRejectCallWhenBusy(shouldReject: Boolean) {
131
+ debugLog(
132
+ TAG,
133
+ "[module] setShouldRejectCallWhenBusy: Updating rejectCallWhenBusy to $shouldReject"
134
+ )
135
+ SettingsStore.setShouldRejectCallWhenBusy(reactApplicationContext, shouldReject)
136
+ }
137
+
141
138
  fun setupAndroid(options: ReadableMap) {
142
139
  debugLog(TAG, "[module] setupAndroid: Setting up Android: $options")
143
140
  val notificationsConfig =
@@ -145,6 +142,18 @@ class CallingxModuleImpl(
145
142
  notificationChannelsManager.setNotificationsConfig(notificationsConfig)
146
143
  notificationChannelsManager.createNotificationChannels()
147
144
 
145
+ val notificationTexts = options.getMap("notificationTexts")
146
+ if (notificationTexts != null) {
147
+ val acceptingText = notificationTexts.getString("accepting")
148
+ val rejectingText = notificationTexts.getString("rejecting")
149
+ debugLog(TAG, "[module] $acceptingText $rejectingText")
150
+ SettingsStore.setOptimisticTexts(
151
+ reactApplicationContext,
152
+ acceptingText,
153
+ rejectingText,
154
+ )
155
+ }
156
+
148
157
  isModuleInitialized = true
149
158
  }
150
159
 
@@ -152,13 +161,30 @@ class CallingxModuleImpl(
152
161
  return notificationChannelsManager.getNotificationStatus().canPost
153
162
  }
154
163
 
164
+ fun stopService(promise: Promise) {
165
+ debugLog(TAG, "[module] stopService: Stopping CallService explicitly from JS")
166
+ try {
167
+ Intent(reactApplicationContext, CallService::class.java)
168
+ .apply { action = CallService.ACTION_STOP_SERVICE }
169
+ .also { reactApplicationContext.startService(it) }
170
+
171
+ promise.resolve(true)
172
+ } catch (e: Exception) {
173
+ Log.e(TAG, "[module] stopService: Failed to stop service: ${e.message}", e)
174
+ promise.reject("STOP_SERVICE_ERROR", e.message, e)
175
+ }
176
+ }
177
+
155
178
  fun getInitialEvents(): WritableArray {
156
- // NOTE: writabel native array can be consumed only once, think of getting rid from clear
157
- // event and clear eat immidiate after getting initial events
179
+ CallEventBus.drainPendingEvents().forEach { onCallEvent(it) }
180
+
181
+ // NOTE: writable native array can be consumed only once, think of getting rid from clear
182
+ // event and clear it immediately after getting initial events
158
183
  val events = delayedEvents
159
184
  debugLog(TAG, "[module] getInitialEvents: Getting initial events: $events")
160
185
  delayedEvents = WritableNativeArray()
161
186
  canSendEvents = true
187
+ CallEventBus.markJsReady()
162
188
  return events
163
189
  }
164
190
 
@@ -184,30 +210,7 @@ class CallingxModuleImpl(
184
210
  return
185
211
  }
186
212
 
187
- trackedCallIds.add(callId)
188
-
189
- // Store the promise keyed by callId; it will be resolved when CALL_REGISTERED_INCOMING_ACTION
190
- // broadcast is received, or rejected on timeout / registration failure.
191
- synchronized(pendingDisplayPromises) {
192
- // Cancel any existing timeout for this callId to avoid a stale
193
- // runnable rejecting the new promise after it overwrites the old one.
194
- pendingTimeouts.remove(callId)?.let { mainHandler.removeCallbacks(it) }
195
-
196
- pendingDisplayPromises[callId] = promise
197
-
198
- // Per-call timeout runnable
199
- val timeoutRunnable = Runnable {
200
- synchronized(pendingDisplayPromises) {
201
- pendingDisplayPromises.remove(callId)?.reject(
202
- "TIMEOUT",
203
- "Timed out waiting for call registration: $callId"
204
- )
205
- pendingTimeouts.remove(callId)
206
- }
207
- }
208
- pendingTimeouts[callId] = timeoutRunnable
209
- mainHandler.postDelayed(timeoutRunnable, displayTimeoutMs)
210
- }
213
+ CallRegistrationStore.trackCallRegistration(callId, promise)
211
214
 
212
215
  try {
213
216
  startCallService(
@@ -220,12 +223,12 @@ class CallingxModuleImpl(
220
223
  )
221
224
  } catch (e: Exception) {
222
225
  Log.e(TAG, "[module] displayIncomingCall: Failed to start foreground service: ${e.message}", e)
223
- trackedCallIds.remove(callId)
224
- synchronized(pendingDisplayPromises) {
225
- pendingTimeouts.remove(callId)?.let { mainHandler.removeCallbacks(it) }
226
- pendingDisplayPromises.remove(callId)
227
- }
228
- promise.reject("START_FOREGROUND_SERVICE_ERROR", e.message, e)
226
+ CallRegistrationStore.reportRegistrationFail(
227
+ callId,
228
+ "START_FOREGROUND_SERVICE_ERROR",
229
+ e.message,
230
+ e
231
+ )
229
232
  }
230
233
  }
231
234
 
@@ -256,7 +259,7 @@ class CallingxModuleImpl(
256
259
  return
257
260
  }
258
261
 
259
- trackedCallIds.add(callId)
262
+ CallRegistrationStore.trackCallRegistration(callId, promise)
260
263
 
261
264
  try {
262
265
  startCallService(
@@ -267,11 +270,14 @@ class CallingxModuleImpl(
267
270
  hasVideo,
268
271
  displayOptions
269
272
  )
270
- promise.resolve(true)
271
273
  } catch (e: Exception) {
272
274
  Log.e(TAG, "[module] startCall: Failed to start foreground service: ${e.message}", e)
273
- trackedCallIds.remove(callId)
274
- promise.reject("START_FOREGROUND_SERVICE_ERROR", e.message, e)
275
+ CallRegistrationStore.reportRegistrationFail(
276
+ callId,
277
+ "START_FOREGROUND_SERVICE_ERROR",
278
+ e.message,
279
+ e
280
+ )
275
281
  }
276
282
  }
277
283
 
@@ -307,28 +313,24 @@ class CallingxModuleImpl(
307
313
 
308
314
  fun endCallWithReason(callId: String, reason: Double, promise: Promise) {
309
315
  debugLog(TAG, "[module] endCallWithReason: Ending call: $callId, $reason")
310
- trackedCallIds.remove(callId)
316
+ CallRegistrationStore.removeTrackedCall(callId)
311
317
  val action = CallAction.Disconnect(DisconnectCause(reason.toInt()))
312
318
  executeServiceAction(callId, action, promise)
313
319
  }
314
320
 
315
321
  fun endCall(callId: String, promise: Promise) {
316
322
  debugLog(TAG, "[module] endCall: Ending call: $callId")
317
- trackedCallIds.remove(callId)
323
+ CallRegistrationStore.removeTrackedCall(callId)
318
324
  val action = CallAction.Disconnect(DisconnectCause(DisconnectCause.LOCAL))
319
325
  executeServiceAction(callId, action, promise)
320
326
  }
321
327
 
322
328
  fun isCallTracked(callId: String): Boolean {
323
- val isTracked = trackedCallIds.contains(callId)
324
- debugLog(TAG, "[module] isCallTracked: Is call tracked: $isTracked")
325
- return isTracked
329
+ return CallRegistrationStore.isCallTracked(callId)
326
330
  }
327
331
 
328
332
  fun hasRegisteredCall(): Boolean {
329
- val hasRegisteredCall = callService?.hasRegisteredCall() ?: false
330
- debugLog(TAG, "[module] hasRegisteredCall: Has registered call: $hasRegisteredCall")
331
- return hasRegisteredCall
333
+ return CallRegistrationStore.hasRegisteredCall()
332
334
  }
333
335
 
334
336
  fun setMutedCall(callId: String, isMuted: Boolean, promise: Promise) {
@@ -352,12 +354,12 @@ class CallingxModuleImpl(
352
354
  putExtra(CallService.EXTRA_TASK_DATA, Bundle())
353
355
  putExtra(CallService.EXTRA_TASK_TIMEOUT, timeout.toLong())
354
356
  }
355
- .also { ContextCompat.startForegroundService(reactApplicationContext, it) }
357
+ .also { reactApplicationContext.startService(it) }
356
358
 
357
359
  promise.resolve(true)
358
360
  } catch (e: Exception) {
359
- Log.e(TAG, "[module] startBackgroundTask: Failed to start foreground service: ${e.message}", e)
360
- promise.reject("START_FOREGROUND_SERVICE_ERROR", e.message, e)
361
+ Log.e(TAG, "[module] startBackgroundTask: Failed to start service: ${e.message}", e)
362
+ promise.reject("START_SERVICE_ERROR", e.message, e)
361
363
  }
362
364
  }
363
365
 
@@ -368,13 +370,13 @@ class CallingxModuleImpl(
368
370
  this.action = CallService.ACTION_STOP_BACKGROUND_TASK
369
371
  putExtra(CallService.EXTRA_TASK_NAME, taskName)
370
372
  }
371
- .also { ContextCompat.startForegroundService(reactApplicationContext, it) }
373
+ .also { reactApplicationContext.startService(it) }
372
374
 
373
375
  isHeadlessTaskRegistered = false
374
376
  promise.resolve(true)
375
377
  } catch (e: Exception) {
376
- Log.e(TAG, "[module] stopBackgroundTask: Failed to start foreground service: ${e.message}", e)
377
- promise.reject("START_FOREGROUND_SERVICE_ERROR", e.message, e)
378
+ Log.e(TAG, "[module] stopBackgroundTask: Failed to start service: ${e.message}", e)
379
+ promise.reject("START_SERVICE_ERROR", e.message, e)
378
380
  }
379
381
  }
380
382
 
@@ -383,13 +385,13 @@ class CallingxModuleImpl(
383
385
  isHeadlessTaskRegistered = true
384
386
  }
385
387
 
386
- fun isServiceStarted(promise: Promise) {
387
- val isStarted =
388
- bindingState == BindingState.BOUND ||
389
- bindingState == BindingState.BINDING ||
390
- callService?.hasRegisteredCall() == true
391
- debugLog(TAG, "[module] isServiceStarted: Service started: $isStarted")
392
- promise.resolve(isStarted)
388
+
389
+ fun fulfillAnswerCallAction(callId: String, didFail: Boolean) {
390
+ // no-op: Android Telecom doesn't require explicit action fulfillment
391
+ }
392
+
393
+ fun fulfillEndCallAction(callId: String, didFail: Boolean) {
394
+ // no-op: Android Telecom doesn't require explicit action fulfillment
393
395
  }
394
396
 
395
397
  fun log(message: String, level: String) {
@@ -438,9 +440,9 @@ class CallingxModuleImpl(
438
440
  putExtra(CallService.EXTRA_TASK_DATA, Bundle())
439
441
  putExtra(CallService.EXTRA_TASK_TIMEOUT, timeout.toLong())
440
442
  }
441
- .also { ContextCompat.startForegroundService(reactApplicationContext, it) }
443
+ .also { reactApplicationContext.startService(it) }
442
444
  } catch (e: Exception) {
443
- Log.e(TAG, "[module] startBackgroundTaskAutomatically: Failed to start foreground service: ${e.message}", e)
445
+ Log.e(TAG, "[module] startBackgroundTaskAutomatically: Failed to start service: ${e.message}", e)
444
446
  }
445
447
  }
446
448
 
@@ -507,17 +509,8 @@ class CallingxModuleImpl(
507
509
  }
508
510
  }
509
511
 
510
- private fun getReceiverFilter(): IntentFilter =
512
+ private fun getServiceReadyReceiverFilter(): IntentFilter =
511
513
  IntentFilter().apply {
512
- addAction(CALL_REGISTERED_ACTION)
513
- addAction(CALL_REGISTERED_INCOMING_ACTION)
514
- addAction(CALL_ANSWERED_ACTION)
515
- addAction(CALL_ACTIVE_ACTION)
516
- addAction(CALL_INACTIVE_ACTION)
517
- addAction(CALL_MUTED_ACTION)
518
- addAction(CALL_ENDPOINT_CHANGED_ACTION)
519
- addAction(CALL_END_ACTION)
520
- addAction(CALL_REGISTRATION_FAILED_ACTION)
521
514
  addAction(SERVICE_READY_ACTION)
522
515
  }
523
516
 
@@ -596,104 +589,96 @@ class CallingxModuleImpl(
596
589
  }
597
590
  }
598
591
 
599
- private inner class CallEventBroadcastReceiver : BroadcastReceiver() {
600
- override fun onReceive(context: Context, intent: Intent) {
601
- val action = intent.action
602
- val callId = intent.getStringExtra(EXTRA_CALL_ID)
603
-
604
- if (action == null) {
605
- return
606
- }
592
+ override fun onCallEvent(event: CallEvent) {
593
+ val action = event.action
594
+ val extras = event.extras
595
+ val callId = extras.getString(EXTRA_CALL_ID)
607
596
 
608
- debugLog(
609
- TAG,
610
- "[module] onReceive: Received intent: $action callId: $callId callService: ${callService != null}"
611
- )
597
+ debugLog(
598
+ TAG,
599
+ "[module] onCallEvent: Received event: $action callId: $callId callService: ${callService != null}"
600
+ )
612
601
 
613
- if (action == SERVICE_READY_ACTION) {
614
- debugLog(TAG, "[module] onReceive: Service is ready, initiating binding, isHeadlessTaskRegistered: $isHeadlessTaskRegistered")
615
- bindToServiceIfNeeded()
616
- startBackgroundTaskAutomatically(HEADLESS_TASK_NAME, 0L)
617
- return
618
- }
602
+ val params = Arguments.createMap()
603
+ if (callId != null) {
604
+ params.putString("callId", callId)
605
+ }
619
606
 
620
- val params = Arguments.createMap()
621
- if (callId != null) {
622
- params.putString("callId", callId)
607
+ when (action) {
608
+ CALL_REGISTERED_ACTION -> {
609
+ sendJSEvent("didReceiveStartCallAction", params)
610
+ if (callId != null) {
611
+ CallRegistrationStore.onRegistrationSuccess(callId)
612
+ }
623
613
  }
624
-
625
- when (action) {
626
- CALL_REGISTERED_ACTION -> {
627
- sendJSEvent("didReceiveStartCallAction", params)
614
+ CALL_REGISTERED_INCOMING_ACTION -> {
615
+ if (callId != null) {
616
+ CallRegistrationStore.onRegistrationSuccess(callId)
628
617
  }
629
- CALL_REGISTERED_INCOMING_ACTION -> {
630
- // Resolve the pending displayIncomingCall promise for this callId
631
- if (callId != null) {
632
- synchronized(pendingDisplayPromises) {
633
- pendingTimeouts.remove(callId)?.let { mainHandler.removeCallbacks(it) }
634
- pendingDisplayPromises.remove(callId)?.resolve(true)
635
- }
636
- }
637
- sendJSEvent("didDisplayIncomingCall", params)
618
+ sendJSEvent("didDisplayIncomingCall", params)
619
+ }
620
+ CALL_REGISTRATION_FAILED_ACTION -> {
621
+ if (callId != null) {
622
+ CallRegistrationStore.onRegistrationFailed(callId)
638
623
  }
639
- CALL_REGISTRATION_FAILED_ACTION -> {
640
- if (callId != null) {
641
- trackedCallIds.remove(callId)
642
- }
643
- // Reject the pending displayIncomingCall promise for this callId
644
- if (callId != null) {
645
- synchronized(pendingDisplayPromises) {
646
- pendingTimeouts.remove(callId)?.let { mainHandler.removeCallbacks(it) }
647
- pendingDisplayPromises.remove(callId)?.reject(
648
- "REGISTRATION_FAILED",
649
- "Failed to register call with telecom: $callId"
650
- )
651
- }
652
- }
624
+ }
625
+ CALL_ANSWERED_ACTION -> {
626
+ if (extras.containsKey(EXTRA_SOURCE)) {
627
+ params.putString("source", extras.getString(EXTRA_SOURCE))
653
628
  }
654
- CALL_ANSWERED_ACTION -> {
655
- if (intent.hasExtra(EXTRA_SOURCE)) {
656
- params.putString("source", intent.getStringExtra(EXTRA_SOURCE))
657
- }
658
- sendJSEvent("answerCall", params)
629
+ sendJSEvent("answerCall", params)
630
+ }
631
+ CALL_END_ACTION -> {
632
+ val source = extras.getString(EXTRA_SOURCE)
633
+ if (source != null) {
634
+ params.putString("source", source)
659
635
  }
660
- CALL_END_ACTION -> {
661
- val source = intent.getStringExtra(EXTRA_SOURCE)
662
- if (source != null) {
663
- params.putString("source", source)
636
+ if (source == "app") {
637
+ if (callId != null) {
638
+ CallRegistrationStore.removeTrackedCall(callId)
664
639
  }
665
- if (source == "app") {
666
- if (callId != null) {
667
- //we stop tracking the call only when it was handled by the app
668
- //in case source is "sys" we still need to know that call is tracked, otherwise we'll unable to end call from js side
669
- trackedCallIds.remove(callId)
670
- }
671
- // means the call was disconnected, we're ready to unbind the service
640
+ // Only unbind when no more calls are tracked
641
+ if (!CallRegistrationStore.hasRegisteredCall()) {
672
642
  unbindServiceSafely()
673
643
  }
674
- params.putString("cause", intent.getStringExtra(EXTRA_DISCONNECT_CAUSE))
675
- sendJSEvent("endCall", params)
676
- }
677
- CALL_INACTIVE_ACTION -> {
678
- params.putBoolean("hold", true)
679
- sendJSEvent("didToggleHoldCallAction", params)
680
644
  }
681
- CALL_ACTIVE_ACTION -> {
682
- params.putBoolean("hold", false)
683
- sendJSEvent("didToggleHoldCallAction", params)
684
- }
685
- CALL_MUTED_ACTION -> {
686
- if (intent.hasExtra(EXTRA_MUTED)) {
687
- params.putBoolean("muted", intent.getBooleanExtra(EXTRA_MUTED, false))
688
- }
689
- sendJSEvent("didPerformSetMutedCallAction", params)
645
+ params.putString("cause", extras.getString(EXTRA_DISCONNECT_CAUSE))
646
+ sendJSEvent("endCall", params)
647
+ }
648
+ CALL_INACTIVE_ACTION -> {
649
+ params.putBoolean("hold", true)
650
+ sendJSEvent("didToggleHoldCallAction", params)
651
+ }
652
+ CALL_ACTIVE_ACTION -> {
653
+ params.putBoolean("hold", false)
654
+ sendJSEvent("didToggleHoldCallAction", params)
655
+ }
656
+ CALL_MUTED_ACTION -> {
657
+ if (extras.containsKey(EXTRA_MUTED)) {
658
+ params.putBoolean("muted", extras.getBoolean(EXTRA_MUTED, false))
690
659
  }
691
- CALL_ENDPOINT_CHANGED_ACTION -> {
692
- if (intent.hasExtra(EXTRA_AUDIO_ENDPOINT)) {
693
- params.putString("output", intent.getStringExtra(EXTRA_AUDIO_ENDPOINT))
694
- }
695
- sendJSEvent("didChangeAudioRoute", params)
660
+ sendJSEvent("didPerformSetMutedCallAction", params)
661
+ }
662
+ CALL_ENDPOINT_CHANGED_ACTION -> {
663
+ if (extras.containsKey(EXTRA_AUDIO_ENDPOINT)) {
664
+ params.putString("output", extras.getString(EXTRA_AUDIO_ENDPOINT))
696
665
  }
666
+ sendJSEvent("didChangeAudioRoute", params)
667
+ }
668
+ }
669
+ }
670
+
671
+ private inner class ServiceReadyBroadcastReceiver : BroadcastReceiver() {
672
+ override fun onReceive(context: Context, intent: Intent) {
673
+ val action = intent.action ?: return
674
+
675
+ if (action == SERVICE_READY_ACTION) {
676
+ debugLog(
677
+ TAG,
678
+ "[module] ServiceReadyBroadcastReceiver: Service is ready, initiating binding, isHeadlessTaskRegistered: $isHeadlessTaskRegistered"
679
+ )
680
+ bindToServiceIfNeeded()
681
+ startBackgroundTaskAutomatically(HEADLESS_TASK_NAME, 0L)
697
682
  }
698
683
  }
699
684
  }
@@ -0,0 +1,48 @@
1
+ package io.getstream.rn.callingx
2
+
3
+ import android.annotation.SuppressLint
4
+ import com.google.firebase.messaging.RemoteMessage
5
+ import io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService
6
+
7
+ /**
8
+ * Extends React Native Firebase's messaging service to start [CallService] when a
9
+ * data message contains "stream" (e.g. incoming call push), then delegates to the
10
+ * parent so setBackgroundMessageHandler() still runs in JS.
11
+ *
12
+ * Only compiled when the app has @react-native-firebase/app and @react-native-firebase/messaging
13
+ * as dependencies. The app must remove the default [io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService] from
14
+ * the merged manifest so this service is the single FCM handler
15
+ */
16
+ @SuppressLint("MissingFirebaseInstanceTokenRefresh")
17
+ class StreamMessagingService : ReactNativeFirebaseMessagingService() {
18
+
19
+ companion object {
20
+ const val TAG = "[Callingx] StreamMessagingService"
21
+ }
22
+
23
+ override fun onMessageReceived(remoteMessage: RemoteMessage) {
24
+ val data = remoteMessage.data
25
+ debugLog(TAG, "onMessageReceived data = $data")
26
+
27
+ val isSupportedStreamVideoCallRing =
28
+ data["sender"] == "stream.video" && data["type"] == "call.ring"
29
+
30
+ if (isSupportedStreamVideoCallRing) {
31
+ val callCid = data["call_cid"]
32
+ if (callCid.isNullOrEmpty()) {
33
+ debugLog(
34
+ TAG,
35
+ "missing call_cid for call.ring, skipping CallService start",
36
+ )
37
+ } else {
38
+ CallService.startIncomingCallFromPush(applicationContext, data)
39
+ }
40
+ } else {
41
+ debugLog(TAG, "sender or type is not supported, skipping CallService start")
42
+ }
43
+
44
+ // Let React Native Firebase continue its normal processing so
45
+ // setBackgroundMessageHandler() still runs in JS.
46
+ super.onMessageReceived(remoteMessage)
47
+ }
48
+ }
@@ -27,6 +27,7 @@ sealed class Call {
27
27
  val isActive: Boolean,
28
28
  val isOnHold: Boolean,
29
29
  val isMuted: Boolean,
30
+ val isPending: Boolean,
30
31
  val errorCode: Int?,
31
32
  val currentCallEndpoint: CallEndpointCompat?,
32
33
  val availableCallEndpoints: List<CallEndpointCompat>,