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

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 +145 -0
  5. package/android/src/main/java/io/getstream/rn/callingx/CallService.kt +301 -83
  6. package/android/src/main/java/io/getstream/rn/callingx/CallingxModuleImpl.kt +148 -390
  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 +188 -48
  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 +20 -24
  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 -14
  24. package/dist/module/utils/constants.js.map +1 -1
  25. package/dist/typescript/src/CallingxModule.d.ts +4 -2
  26. package/dist/typescript/src/CallingxModule.d.ts.map +1 -1
  27. package/dist/typescript/src/spec/NativeCallingx.d.ts +7 -4
  28. package/dist/typescript/src/spec/NativeCallingx.d.ts.map +1 -1
  29. package/dist/typescript/src/types.d.ts +33 -5
  30. package/dist/typescript/src/types.d.ts.map +1 -1
  31. package/dist/typescript/src/utils/constants.d.ts +2 -3
  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 +20 -21
  41. package/src/spec/NativeCallingx.ts +10 -6
  42. package/src/types.ts +36 -4
  43. package/src/utils/constants.ts +23 -12
  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
@@ -1,16 +1,11 @@
1
1
  package io.getstream.rn.callingx
2
2
 
3
3
  import android.content.BroadcastReceiver
4
- import android.content.ComponentName
5
4
  import android.content.Context
6
5
  import android.content.Intent
7
6
  import android.content.IntentFilter
8
- import android.content.ServiceConnection
9
7
  import android.os.Build
10
8
  import android.os.Bundle
11
- import android.os.Handler
12
- import android.os.IBinder
13
- import android.os.Looper
14
9
  import android.telecom.DisconnectCause
15
10
  import android.util.Log
16
11
  import androidx.core.content.ContextCompat
@@ -27,12 +22,12 @@ import com.facebook.react.modules.core.DeviceEventManagerModule
27
22
  import io.getstream.rn.callingx.model.CallAction
28
23
  import io.getstream.rn.callingx.notifications.NotificationChannelsManager
29
24
  import io.getstream.rn.callingx.notifications.NotificationsConfig
30
- import java.util.concurrent.ConcurrentHashMap
25
+ import io.getstream.rn.callingx.utils.SettingsStore
31
26
 
32
27
  class CallingxModuleImpl(
33
28
  private val reactApplicationContext: ReactApplicationContext,
34
29
  private val eventEmitter: CallingxEventEmitterAdapter
35
- ) {
30
+ ) : CallEventBus.Listener {
36
31
 
37
32
  companion object {
38
33
  const val TAG = "[Callingx] CallingxModule"
@@ -44,100 +39,54 @@ class CallingxModuleImpl(
44
39
  const val EXTRA_DISCONNECT_CAUSE = "disconnect_cause"
45
40
  const val EXTRA_AUDIO_ENDPOINT = "audio_endpoint"
46
41
  const val EXTRA_SOURCE = "source"
47
-
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"
42
+ const val EXTRA_ACTION = "action_name"
43
+
44
+ // Action names must match intent-filter entries in AndroidManifest.xml
45
+ const val CALL_REGISTERED_ACTION = "io.getstream.CALL_REGISTERED"
46
+ const val CALL_REGISTERED_INCOMING_ACTION = "io.getstream.CALL_REGISTERED_INCOMING"
47
+ const val CALL_ANSWERED_ACTION = "io.getstream.CALL_ANSWERED"
48
+ const val CALL_INACTIVE_ACTION = "io.getstream.CALL_INACTIVE"
49
+ const val CALL_ACTIVE_ACTION = "io.getstream.CALL_ACTIVE"
50
+ const val CALL_MUTED_ACTION = "io.getstream.CALL_MUTED"
51
+ const val CALL_ENDPOINT_CHANGED_ACTION = "io.getstream.CALL_ENDPOINT_CHANGED"
52
+ const val CALL_END_ACTION = "io.getstream.CALL_END"
53
+ const val CALL_REGISTRATION_FAILED_ACTION = "io.getstream.CALL_REGISTRATION_FAILED"
54
+ const val CALL_OPTIMISTIC_ACCEPT_ACTION = "io.getstream.ACCEPT_CALL_OPTIMISTIC"
58
55
  // Background task name
59
56
  const val HEADLESS_TASK_NAME = "HandleCallBackgroundState"
60
- const val SERVICE_READY_ACTION = "service_ready"
61
- }
62
-
63
- private enum class BindingState {
64
- UNBOUND,
65
- BINDING,
66
- BOUND
67
57
  }
68
58
 
69
- private var callService: CallService? = null
70
- private var bindingState = BindingState.UNBOUND
71
-
72
59
  private var delayedEvents = WritableNativeArray()
73
60
  private var isModuleInitialized = false
74
61
  private var canSendEvents = false
75
- private var isHeadlessTaskRegistered = false
76
-
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
62
 
87
63
  private val notificationChannelsManager = NotificationChannelsManager(reactApplicationContext)
88
- private val callEventBroadcastReceiver = CallEventBroadcastReceiver()
89
- private val appStateListener =
90
- object : LifecycleEventListener {
91
- override fun onHostResume() {}
92
-
93
- override fun onHostPause() {}
94
-
95
- override fun onHostDestroy() {
96
- // App destroyed - force unbind
97
- debugLog(TAG, "[module] onHostDestroy: App destroyed")
98
- unbindServiceSafely()
99
- }
100
- }
101
64
 
102
65
  init {
103
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
104
- reactApplicationContext.registerReceiver(
105
- callEventBroadcastReceiver,
106
- getReceiverFilter(),
107
- Context.RECEIVER_NOT_EXPORTED
108
- )
109
- } else {
110
- @Suppress("UnspecifiedRegisterReceiverFlag")
111
- reactApplicationContext.registerReceiver(callEventBroadcastReceiver, getReceiverFilter())
112
- }
66
+ CallEventBus.subscribe(this)
113
67
  }
114
68
 
115
69
  fun initialize() {
116
- reactApplicationContext.addLifecycleEventListener(appStateListener)
117
-
118
- tryToBindIfNeeded()
119
-
120
70
  debugLog(TAG, "[module] initialize: Initializing module")
121
71
  }
122
72
 
123
73
  fun invalidate() {
124
74
  debugLog(TAG, "[module] invalidate: Invalidating module")
125
75
 
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()
134
- unbindServiceSafely()
76
+ CallRegistrationStore.clearAll()
77
+ CallEventBus.unsubscribe(this)
135
78
 
136
- reactApplicationContext.removeLifecycleEventListener(appStateListener)
137
- reactApplicationContext.unregisterReceiver(callEventBroadcastReceiver)
138
79
  isModuleInitialized = false
139
80
  }
140
81
 
82
+ fun setShouldRejectCallWhenBusy(shouldReject: Boolean) {
83
+ debugLog(
84
+ TAG,
85
+ "[module] setShouldRejectCallWhenBusy: Updating rejectCallWhenBusy to $shouldReject"
86
+ )
87
+ SettingsStore.setShouldRejectCallWhenBusy(reactApplicationContext, shouldReject)
88
+ }
89
+
141
90
  fun setupAndroid(options: ReadableMap) {
142
91
  debugLog(TAG, "[module] setupAndroid: Setting up Android: $options")
143
92
  val notificationsConfig =
@@ -145,6 +94,18 @@ class CallingxModuleImpl(
145
94
  notificationChannelsManager.setNotificationsConfig(notificationsConfig)
146
95
  notificationChannelsManager.createNotificationChannels()
147
96
 
97
+ val notificationTexts = options.getMap("notificationTexts")
98
+ if (notificationTexts != null) {
99
+ val acceptingText = notificationTexts.getString("accepting")
100
+ val rejectingText = notificationTexts.getString("rejecting")
101
+ debugLog(TAG, "[module] $acceptingText $rejectingText")
102
+ SettingsStore.setOptimisticTexts(
103
+ reactApplicationContext,
104
+ acceptingText,
105
+ rejectingText,
106
+ )
107
+ }
108
+
148
109
  isModuleInitialized = true
149
110
  }
150
111
 
@@ -152,13 +113,30 @@ class CallingxModuleImpl(
152
113
  return notificationChannelsManager.getNotificationStatus().canPost
153
114
  }
154
115
 
116
+ fun stopService(promise: Promise) {
117
+ debugLog(TAG, "[module] stopService: Stopping CallService explicitly from JS")
118
+ try {
119
+ Intent(reactApplicationContext, CallService::class.java)
120
+ .apply { action = CallService.ACTION_STOP_SERVICE }
121
+ .also { reactApplicationContext.startService(it) }
122
+
123
+ promise.resolve(true)
124
+ } catch (e: Exception) {
125
+ Log.e(TAG, "[module] stopService: Failed to stop service: ${e.message}", e)
126
+ promise.reject("STOP_SERVICE_ERROR", e.message, e)
127
+ }
128
+ }
129
+
155
130
  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
131
+ CallEventBus.drainPendingEvents().forEach { onCallEvent(it) }
132
+
133
+ // NOTE: writable native array can be consumed only once, think of getting rid from clear
134
+ // event and clear it immediately after getting initial events
158
135
  val events = delayedEvents
159
136
  debugLog(TAG, "[module] getInitialEvents: Getting initial events: $events")
160
137
  delayedEvents = WritableNativeArray()
161
138
  canSendEvents = true
139
+ CallEventBus.markJsReady()
162
140
  return events
163
141
  }
164
142
 
@@ -184,30 +162,7 @@ class CallingxModuleImpl(
184
162
  return
185
163
  }
186
164
 
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
- }
165
+ CallRegistrationStore.trackCallRegistration(callId, promise)
211
166
 
212
167
  try {
213
168
  startCallService(
@@ -220,12 +175,12 @@ class CallingxModuleImpl(
220
175
  )
221
176
  } catch (e: Exception) {
222
177
  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)
178
+ CallRegistrationStore.reportRegistrationFail(
179
+ callId,
180
+ "START_FOREGROUND_SERVICE_ERROR",
181
+ e.message,
182
+ e
183
+ )
229
184
  }
230
185
  }
231
186
 
@@ -256,7 +211,7 @@ class CallingxModuleImpl(
256
211
  return
257
212
  }
258
213
 
259
- trackedCallIds.add(callId)
214
+ CallRegistrationStore.trackCallRegistration(callId, promise)
260
215
 
261
216
  try {
262
217
  startCallService(
@@ -267,11 +222,14 @@ class CallingxModuleImpl(
267
222
  hasVideo,
268
223
  displayOptions
269
224
  )
270
- promise.resolve(true)
271
225
  } catch (e: Exception) {
272
226
  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)
227
+ CallRegistrationStore.reportRegistrationFail(
228
+ callId,
229
+ "START_FOREGROUND_SERVICE_ERROR",
230
+ e.message,
231
+ e
232
+ )
275
233
  }
276
234
  }
277
235
 
@@ -307,28 +265,24 @@ class CallingxModuleImpl(
307
265
 
308
266
  fun endCallWithReason(callId: String, reason: Double, promise: Promise) {
309
267
  debugLog(TAG, "[module] endCallWithReason: Ending call: $callId, $reason")
310
- trackedCallIds.remove(callId)
268
+ CallRegistrationStore.removeTrackedCall(callId)
311
269
  val action = CallAction.Disconnect(DisconnectCause(reason.toInt()))
312
270
  executeServiceAction(callId, action, promise)
313
271
  }
314
272
 
315
273
  fun endCall(callId: String, promise: Promise) {
316
274
  debugLog(TAG, "[module] endCall: Ending call: $callId")
317
- trackedCallIds.remove(callId)
275
+ CallRegistrationStore.removeTrackedCall(callId)
318
276
  val action = CallAction.Disconnect(DisconnectCause(DisconnectCause.LOCAL))
319
277
  executeServiceAction(callId, action, promise)
320
278
  }
321
279
 
322
280
  fun isCallTracked(callId: String): Boolean {
323
- val isTracked = trackedCallIds.contains(callId)
324
- debugLog(TAG, "[module] isCallTracked: Is call tracked: $isTracked")
325
- return isTracked
281
+ return CallRegistrationStore.isCallTracked(callId)
326
282
  }
327
283
 
328
284
  fun hasRegisteredCall(): Boolean {
329
- val hasRegisteredCall = callService?.hasRegisteredCall() ?: false
330
- debugLog(TAG, "[module] hasRegisteredCall: Has registered call: $hasRegisteredCall")
331
- return hasRegisteredCall
285
+ return CallRegistrationStore.hasRegisteredCall()
332
286
  }
333
287
 
334
288
  fun setMutedCall(callId: String, isMuted: Boolean, promise: Promise) {
@@ -352,12 +306,12 @@ class CallingxModuleImpl(
352
306
  putExtra(CallService.EXTRA_TASK_DATA, Bundle())
353
307
  putExtra(CallService.EXTRA_TASK_TIMEOUT, timeout.toLong())
354
308
  }
355
- .also { ContextCompat.startForegroundService(reactApplicationContext, it) }
309
+ .also { reactApplicationContext.startService(it) }
356
310
 
357
311
  promise.resolve(true)
358
312
  } 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)
313
+ Log.e(TAG, "[module] startBackgroundTask: Failed to start service: ${e.message}", e)
314
+ promise.reject("START_SERVICE_ERROR", e.message, e)
361
315
  }
362
316
  }
363
317
 
@@ -368,28 +322,26 @@ class CallingxModuleImpl(
368
322
  this.action = CallService.ACTION_STOP_BACKGROUND_TASK
369
323
  putExtra(CallService.EXTRA_TASK_NAME, taskName)
370
324
  }
371
- .also { ContextCompat.startForegroundService(reactApplicationContext, it) }
325
+ .also { reactApplicationContext.startService(it) }
372
326
 
373
- isHeadlessTaskRegistered = false
374
327
  promise.resolve(true)
375
328
  } 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)
329
+ Log.e(TAG, "[module] stopBackgroundTask: Failed to start service: ${e.message}", e)
330
+ promise.reject("START_SERVICE_ERROR", e.message, e)
378
331
  }
379
332
  }
380
333
 
381
334
  fun registerBackgroundTaskAvailable() {
382
335
  debugLog(TAG, "[module] registerBackgroundTaskAvailable: Headless task registered")
383
- isHeadlessTaskRegistered = true
384
336
  }
385
337
 
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)
338
+
339
+ fun fulfillAnswerCallAction(callId: String, didFail: Boolean) {
340
+ // no-op: Android Telecom doesn't require explicit action fulfillment
341
+ }
342
+
343
+ fun fulfillEndCallAction(callId: String, didFail: Boolean) {
344
+ // no-op: Android Telecom doesn't require explicit action fulfillment
393
345
  }
394
346
 
395
347
  fun log(message: String, level: String) {
@@ -421,54 +373,16 @@ class CallingxModuleImpl(
421
373
  .also { ContextCompat.startForegroundService(reactApplicationContext, it) }
422
374
  }
423
375
 
424
- private fun startBackgroundTaskAutomatically(taskName: String, timeout: Long) {
425
- if (!isHeadlessTaskRegistered) {
426
- debugLog(
427
- TAG,
428
- "[module] startBackgroundTaskAutomatically: Headless task is not registered"
429
- )
430
- return
431
- }
432
-
433
- try {
434
- Intent(reactApplicationContext, CallService::class.java)
435
- .apply {
436
- this.action = CallService.ACTION_START_BACKGROUND_TASK
437
- putExtra(CallService.EXTRA_TASK_NAME, taskName)
438
- putExtra(CallService.EXTRA_TASK_DATA, Bundle())
439
- putExtra(CallService.EXTRA_TASK_TIMEOUT, timeout.toLong())
440
- }
441
- .also { ContextCompat.startForegroundService(reactApplicationContext, it) }
442
- } catch (e: Exception) {
443
- Log.e(TAG, "[module] startBackgroundTaskAutomatically: Failed to start foreground service: ${e.message}", e)
444
- }
445
- }
446
-
447
376
  private fun executeServiceAction(callId: String, action: CallAction, promise: Promise) {
448
377
  debugLog(TAG, "[module] executeServiceAction: Executing service action: $action")
449
- when (bindingState) {
450
- BindingState.BOUND -> {
451
- if (callService != null) {
452
- callService?.processAction(callId, action)
453
- promise.resolve(true)
454
- } else {
455
- promise.reject("ERROR", "Service reference lost")
378
+ Intent(reactApplicationContext, CallService::class.java)
379
+ .apply {
380
+ this.action = CallService.ACTION_PROCESS_ACTION
381
+ putExtra(CallService.EXTRA_CALL_ID, callId)
382
+ putExtra(CallService.EXTRA_ACTION, action)
456
383
  }
457
- }
458
- BindingState.BINDING -> {
459
- debugLog(TAG, "executeServiceAction: Service binding, queueing action")
460
- promise.reject(
461
- "SERVICE_BINDING",
462
- "Service is connecting, please try again in a moment"
463
- )
464
- }
465
- BindingState.UNBOUND -> {
466
- promise.reject(
467
- "SERVICE_NOT_CONNECTED",
468
- "Service not connected. Call may not be active."
469
- )
470
- }
471
- }
384
+ .also { reactApplicationContext.startService(it) }
385
+ .also { promise.resolve(true) }
472
386
  }
473
387
 
474
388
  private fun sendJSEvent(eventName: String, params: WritableMap? = null) {
@@ -507,229 +421,73 @@ class CallingxModuleImpl(
507
421
  }
508
422
  }
509
423
 
510
- private fun getReceiverFilter(): IntentFilter =
511
- 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
- addAction(SERVICE_READY_ACTION)
522
- }
523
-
524
- private fun bindToServiceIfNeeded() {
525
- when (bindingState) {
526
- BindingState.BOUND -> {
527
- debugLog(TAG, "[module] bindToServiceIfNeeded: Already bound")
528
- return
529
- }
530
- BindingState.BINDING -> {
531
- debugLog(TAG, "[module] bindToServiceIfNeeded: Already binding")
532
- return
533
- }
534
- BindingState.UNBOUND -> {
535
- debugLog(TAG, "[module] bindToServiceIfNeeded: Attempting to bind")
536
- val intent = Intent(reactApplicationContext, CallService::class.java)
537
- try {
538
- val success =
539
- reactApplicationContext.bindService(
540
- intent,
541
- serviceConnection,
542
- Context.BIND_AUTO_CREATE or Context.BIND_IMPORTANT
543
- )
544
- if (success) {
545
- bindingState = BindingState.BINDING
546
- debugLog(TAG, "[module] bindToServiceIfNeeded: Bind request successful")
547
- } else {
548
- Log.e(TAG, "[module] bindToServiceIfNeeded: Bind request failed")
549
- bindingState = BindingState.UNBOUND
550
- }
551
- } catch (e: Exception) {
552
- Log.e(TAG, "[module] bindToServiceIfNeeded: Exception during bind", e)
553
- bindingState = BindingState.UNBOUND
554
- }
555
- }
556
- }
557
- }
558
-
559
- private fun unbindServiceSafely() {
560
- debugLog(TAG, "[module] unbindServiceSafely: Unbinding service")
561
- if (bindingState == BindingState.BOUND || bindingState == BindingState.BINDING) {
562
- try {
563
- reactApplicationContext.unbindService(serviceConnection)
564
- debugLog(TAG, "[module] unbindServiceSafely: Successfully unbound")
565
- } catch (e: IllegalArgumentException) {
566
- Log.w(
567
- TAG,
568
- "[module] unbindServiceSafely: Service not registered or already unbound"
569
- )
570
- } catch (e: Exception) {
571
- Log.e(TAG, "[module] unbindServiceSafely: Error unbinding service", e)
572
- } finally {
573
- bindingState = BindingState.UNBOUND
574
- callService = null
575
- }
576
- }
577
- }
424
+ override fun onCallEvent(event: CallEvent) {
425
+ val action = event.action
426
+ val extras = event.extras
427
+ val callId = extras.getString(EXTRA_CALL_ID)
578
428
 
579
- private fun tryToBindIfNeeded() {
580
- val intent = Intent(reactApplicationContext, CallService::class.java)
581
- try {
582
- val success =
583
- reactApplicationContext.bindService(
584
- intent,
585
- serviceConnection,
586
- 0 // No flags - only bind if service exists
587
- )
588
- if (success) {
589
- bindingState = BindingState.BINDING
590
- debugLog(TAG, "[module] checkForExistingService: Service exists, binding")
591
- } else {
592
- debugLog(TAG, "[module] checkForExistingService: No existing service")
593
- }
594
- } catch (e: Exception) {
595
- Log.e(TAG, "[module] checkForExistingService: Error checking for service", e)
429
+ val params = Arguments.createMap()
430
+ if (callId != null) {
431
+ params.putString("callId", callId)
596
432
  }
597
- }
598
-
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
433
 
604
- if (action == null) {
605
- return
434
+ when (action) {
435
+ CALL_REGISTERED_ACTION -> {
436
+ sendJSEvent("didReceiveStartCallAction", params)
437
+ if (callId != null) {
438
+ CallRegistrationStore.onRegistrationSuccess(callId)
439
+ }
606
440
  }
607
-
608
- debugLog(
609
- TAG,
610
- "[module] onReceive: Received intent: $action callId: $callId callService: ${callService != null}"
611
- )
612
-
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
441
+ CALL_REGISTERED_INCOMING_ACTION -> {
442
+ if (callId != null) {
443
+ CallRegistrationStore.onRegistrationSuccess(callId)
444
+ }
445
+ sendJSEvent("didDisplayIncomingCall", params)
618
446
  }
619
-
620
- val params = Arguments.createMap()
621
- if (callId != null) {
622
- params.putString("callId", callId)
447
+ CALL_REGISTRATION_FAILED_ACTION -> {
448
+ if (callId != null) {
449
+ CallRegistrationStore.onRegistrationFailed(callId)
450
+ }
623
451
  }
624
-
625
- when (action) {
626
- CALL_REGISTERED_ACTION -> {
627
- sendJSEvent("didReceiveStartCallAction", params)
452
+ CALL_ANSWERED_ACTION -> {
453
+ if (extras.containsKey(EXTRA_SOURCE)) {
454
+ params.putString("source", extras.getString(EXTRA_SOURCE))
628
455
  }
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)
456
+ sendJSEvent("answerCall", params)
457
+ }
458
+ CALL_END_ACTION -> {
459
+ val source = extras.getString(EXTRA_SOURCE)
460
+ if (source != null) {
461
+ params.putString("source", source)
638
462
  }
639
- CALL_REGISTRATION_FAILED_ACTION -> {
463
+ if (source == "app") {
640
464
  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
- }
653
- }
654
- CALL_ANSWERED_ACTION -> {
655
- if (intent.hasExtra(EXTRA_SOURCE)) {
656
- params.putString("source", intent.getStringExtra(EXTRA_SOURCE))
657
- }
658
- sendJSEvent("answerCall", params)
659
- }
660
- CALL_END_ACTION -> {
661
- val source = intent.getStringExtra(EXTRA_SOURCE)
662
- if (source != null) {
663
- params.putString("source", source)
664
- }
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
672
- unbindServiceSafely()
465
+ CallRegistrationStore.removeTrackedCall(callId)
673
466
  }
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
467
  }
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)
468
+ params.putString("cause", extras.getString(EXTRA_DISCONNECT_CAUSE))
469
+ sendJSEvent("endCall", params)
470
+ }
471
+ CALL_INACTIVE_ACTION -> {
472
+ params.putBoolean("hold", true)
473
+ sendJSEvent("didToggleHoldCallAction", params)
474
+ }
475
+ CALL_ACTIVE_ACTION -> {
476
+ params.putBoolean("hold", false)
477
+ sendJSEvent("didToggleHoldCallAction", params)
478
+ }
479
+ CALL_MUTED_ACTION -> {
480
+ if (extras.containsKey(EXTRA_MUTED)) {
481
+ params.putBoolean("muted", extras.getBoolean(EXTRA_MUTED, false))
690
482
  }
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)
483
+ sendJSEvent("didPerformSetMutedCallAction", params)
484
+ }
485
+ CALL_ENDPOINT_CHANGED_ACTION -> {
486
+ if (extras.containsKey(EXTRA_AUDIO_ENDPOINT)) {
487
+ params.putString("output", extras.getString(EXTRA_AUDIO_ENDPOINT))
696
488
  }
489
+ sendJSEvent("didChangeAudioRoute", params)
697
490
  }
698
491
  }
699
492
  }
700
-
701
- private val serviceConnection =
702
- object : ServiceConnection {
703
- override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
704
- debugLog(TAG, "[module] onServiceConnected: Service connected")
705
- val binder = service as? CallService.CallServiceBinder
706
- callService = binder?.getService()
707
- bindingState = BindingState.BOUND
708
- }
709
-
710
- override fun onServiceDisconnected(name: ComponentName?) {
711
- debugLog(TAG, "onServiceDisconnected: Service disconnected unexpectedly")
712
- callService = null
713
- bindingState = BindingState.UNBOUND
714
- }
715
-
716
- override fun onBindingDied(name: ComponentName?) {
717
- Log.e(TAG, "[module] onBindingDied: Service binding died")
718
- callService = null
719
- bindingState = BindingState.UNBOUND
720
-
721
- // Must unbind to clean up the dead binding
722
- try {
723
- reactApplicationContext.unbindService(this)
724
- } catch (e: Exception) {
725
- Log.w(TAG, "[module] onBindingDied: Error unbinding dead connection", e)
726
- }
727
- }
728
-
729
- override fun onNullBinding(name: ComponentName?) {
730
- Log.e(TAG, "[module] onNullBinding: Service returned null binding")
731
- bindingState = BindingState.UNBOUND
732
- callService = null
733
- }
734
- }
735
493
  }