@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
@@ -19,17 +19,26 @@ import kotlinx.coroutines.cancel
19
19
  import kotlinx.coroutines.channels.Channel
20
20
  import kotlinx.coroutines.flow.Flow
21
21
  import kotlinx.coroutines.flow.consumeAsFlow
22
- import kotlinx.coroutines.flow.drop
23
22
  import kotlinx.coroutines.flow.launchIn
24
23
  import kotlinx.coroutines.flow.onEach
25
- import kotlinx.coroutines.flow.scan
26
24
  import kotlinx.coroutines.launch
27
25
  import kotlinx.coroutines.sync.withLock
26
+ import java.util.concurrent.ConcurrentHashMap
27
+ import java.util.concurrent.atomic.AtomicBoolean
28
28
 
29
29
  /**
30
- * The central repository that keeps track of the current call and allows to register new calls.
30
+ * Per-call flags tracking whether an action was initiated by the app (self) or by the system.
31
+ */
32
+ private data class CallActionFlags(
33
+ val isSelfAnswered: AtomicBoolean = AtomicBoolean(false),
34
+ val isSelfDisconnected: AtomicBoolean = AtomicBoolean(false),
35
+ )
36
+
37
+ /**
38
+ * The central repository that keeps track of calls and allows to register new ones.
31
39
  *
32
40
  * This class contains the main logic to integrate with Telecom SDK.
41
+ * Multiple calls can be registered simultaneously — each gets its own [CallControlScope].
33
42
  *
34
43
  * @see registerCall
35
44
  */
@@ -40,11 +49,15 @@ class TelecomCallRepository(context: Context) : CallRepository(context) {
40
49
  private const val TAG = "[Callingx] TelecomCallRepository"
41
50
  }
42
51
 
43
- private var observeCallStateJob: Job? = null
52
+ @Volatile
53
+ private var isReleased: Boolean = false
54
+
55
+ private var observeCallsJob: Job? = null
44
56
 
45
57
  private val callsManager: CallsManager
46
- private var isSelfAnswered = false
47
- private var isSelfDisconnected = false
58
+
59
+ /** Per-call action-source flags, keyed by callId. */
60
+ private val actionFlags = ConcurrentHashMap<String, CallActionFlags>()
48
61
 
49
62
  init {
50
63
  val capabilities =
@@ -62,26 +75,34 @@ class TelecomCallRepository(context: Context) : CallRepository(context) {
62
75
  override fun setListener(listener: Listener?) {
63
76
  this._listener = listener
64
77
 
65
- observeCallStateJob?.cancel()
66
- observeCallStateJob = observeCallState()
78
+ observeCallsJob?.cancel()
79
+ observeCallsJob = observeCalls()
67
80
  }
68
81
 
69
82
  override fun release() {
70
- val currentCall = currentCall.value
71
- if (currentCall is Call.Registered) {
72
- currentCall.processAction(CallAction.Disconnect(DisconnectCause(DisconnectCause.LOCAL)))
83
+ if (isReleased) {
84
+ debugLog(TAG, "[repository] release: Already released, ignoring")
85
+ return
86
+ }
87
+ isReleased = true
88
+
89
+ // Disconnect all active calls
90
+ val currentCalls = _calls.value
91
+ for ((callId, call) in currentCalls) {
92
+ call.processAction(CallAction.Disconnect(DisconnectCause(DisconnectCause.LOCAL)))
73
93
  }
74
- _currentCall.value = Call.None
94
+ _calls.value = emptyMap()
95
+ actionFlags.clear()
75
96
 
76
- observeCallStateJob?.cancel()
77
- observeCallStateJob = null
97
+ observeCallsJob?.cancel()
98
+ observeCallsJob = null
78
99
  _listener = null
79
100
 
80
101
  scope.cancel()
81
102
  }
82
103
 
83
104
  /**
84
- * Register a new call with the provided attributes. Use the [currentCall] StateFlow to receive
105
+ * Register a new call with the provided attributes. Use the [calls] StateFlow to receive
85
106
  * status updates and process call related actions.
86
107
  */
87
108
  override suspend fun registerCall(
@@ -92,89 +113,119 @@ class TelecomCallRepository(context: Context) : CallRepository(context) {
92
113
  isVideo: Boolean,
93
114
  displayOptions: Bundle?,
94
115
  ) {
95
- debugLog(
96
- TAG,
97
- "[repository] registerCall: Starting registration - Name: $displayName, Address: $address, Incoming: $isIncoming"
98
- )
116
+ if (isReleased) {
117
+ Log.w(
118
+ TAG,
119
+ "[repository] registerCall: Repository already released, ignoring registration for $callId"
120
+ )
121
+ return
122
+ }
123
+
124
+ // Hold the mutex only for the dedup check — release before entering the long-lived call scope
125
+ val attributes: CallAttributesCompat
126
+ val actionSource: Channel<CallAction>
127
+ val flags: CallActionFlags
99
128
 
100
129
  registrationMutex.withLock {
101
- // For simplicity we don't support multiple calls
102
- if (_currentCall.value is Call.Registered) {
130
+ debugLog(
131
+ TAG,
132
+ "[repository] registerCall: Starting registration - CallId: $callId, Name: $displayName, Address: $address, Incoming: $isIncoming"
133
+ )
134
+
135
+ // Check if this specific call is already registered or registration is in progress
136
+ if (_calls.value.containsKey(callId)) {
103
137
  Log.w(
104
138
  TAG,
105
- "[repository] registerCall: Call already registered, ignoring new call request"
139
+ "[repository] registerCall: Call $callId already registered, ignoring duplicate"
106
140
  )
107
- return@withLock
141
+ return
108
142
  }
143
+
109
144
  debugLog(
110
145
  TAG,
111
- "[repository] registerCall: No existing call found, proceeding with registration"
146
+ "[repository] registerCall: Call $callId not found in map, proceeding with registration"
112
147
  )
113
148
 
114
- val attributes = createCallAttributes(displayName, address, isIncoming, isVideo)
115
- val actionSource = Channel<CallAction>()
116
-
117
- // Register the call and handle actions in the scope
118
- try {
119
- callsManager.addCall(
120
- attributes,
121
- onIsCallAnswered, // Watch needs to know if it can answer the call
122
- onIsCallDisconnected,
123
- onIsCallActive,
124
- onIsCallInactive
125
- ) {
126
- debugLog(
127
- TAG,
128
- "[repository] registerCall: Inside call scope, setting up call handlers"
129
- )
149
+ attributes = createCallAttributes(displayName, address, isIncoming, isVideo)
150
+ actionSource = Channel<CallAction>()
151
+ flags = CallActionFlags()
152
+ actionFlags[callId] = flags
153
+
154
+ // Add call to the map early so that duplicate registrations are rejected
155
+ // and listeners are notified immediately. Actions sent via trySend() may arrive
156
+ // before the call scope starts collecting; in that case, they are dropped
157
+ // rather than buffered, which is acceptable because we explicitly handle
158
+ // pending actions in CallService/CallRegistrationStore.
159
+ val registeredCall = Call.Registered(
160
+ id = callId,
161
+ isPending = true,
162
+ isActive = false,
163
+ isOnHold = false,
164
+ callAttributes = attributes,
165
+ displayOptions = displayOptions,
166
+ isMuted = false,
167
+ errorCode = null,
168
+ currentCallEndpoint = null,
169
+ availableCallEndpoints = emptyList(),
170
+ actionSource = actionSource,
171
+ )
172
+ addCall(callId, registeredCall)
173
+ debugLog(TAG, "[repository] registerCall: Call $callId added to map in pending state")
174
+ }
175
+
176
+ // Register the call with Telecom and handle actions in the scope (mutex released)
177
+ try {
178
+ callsManager.addCall(
179
+ attributes,
180
+ onIsCallAnswered(callId, flags),
181
+ onIsCallDisconnected(callId, flags),
182
+ onIsCallActive(callId),
183
+ onIsCallInactive(callId)
184
+ ) {
185
+ debugLog(
186
+ TAG,
187
+ "[repository] registerCall: Inside call scope for $callId, setting up call handlers"
188
+ )
130
189
 
131
- // Consume the actions to interact with the call inside the scope
132
- launch { processCallActions(actionSource.consumeAsFlow()) }
190
+ // Call is now registered in Telecom mark as no longer pending
191
+ updateCallById(callId) { copy(isPending = false) }
133
192
 
134
- // Update the state to registered with default values while waiting for Telecom
135
- // updates
136
- debugLog(
137
- TAG,
138
- "[repository] registerCall: Creating Registered call state with ID: $callId"
139
- )
140
- _currentCall.value =
141
- Call.Registered(
142
- id = callId,
143
- isActive = false, // can we just register the call as active?
144
- isOnHold = false,
145
- callAttributes = attributes,
146
- displayOptions = displayOptions,
147
- isMuted = false,
148
- errorCode = null,
149
- currentCallEndpoint = null,
150
- availableCallEndpoints = emptyList(),
151
- actionSource = actionSource,
152
- )
153
- debugLog(TAG, "[repository] registerCall: Call state updated to Registered")
193
+ // Consume the actions to interact with the call inside the scope
194
+ launch { processCallActions(callId, flags, actionSource.consumeAsFlow()) }
154
195
 
155
- launch {
156
- currentCallEndpoint.collect {
157
- updateCurrentCall { copy(currentCallEndpoint = it) }
158
- }
196
+ launch {
197
+ currentCallEndpoint.collect {
198
+ updateCallById(callId) { copy(currentCallEndpoint = it) }
159
199
  }
160
- launch {
161
- availableEndpoints.collect {
162
- updateCurrentCall { copy(availableCallEndpoints = it) }
163
- }
200
+ }
201
+ launch {
202
+ availableEndpoints.collect {
203
+ updateCallById(callId) { copy(availableCallEndpoints = it) }
204
+ }
205
+ }
206
+ launch {
207
+ isMuted.collect {
208
+ updateCallById(callId) { copy(isMuted = it) }
164
209
  }
165
- launch { isMuted.collect { updateCurrentCall { copy(isMuted = it) } } }
166
210
  }
167
- debugLog(
168
- TAG,
169
- "[repository] registerCall: Call successfully registered with Telecom SDK"
170
- )
171
- } catch (e: Exception) {
172
- Log.e(TAG, "[repository] registerCall: Error registering call", e)
173
- throw e
174
- } finally {
175
- debugLog(TAG, "[repository] registerCall: Call scope ended, setting state to None")
176
- _currentCall.value = Call.None
177
211
  }
212
+ debugLog(
213
+ TAG,
214
+ "[repository] registerCall: Call $callId scope ended normally"
215
+ )
216
+ } catch (e: Exception) {
217
+ Log.e(TAG, "[repository] registerCall: Error registering call $callId", e)
218
+ throw e
219
+ } finally {
220
+ // Call lifecycle cleanup:
221
+ // - processCallActions(...) is launched in the CallControlScope, so its collector is
222
+ // cancelled automatically when the Telecom call scope finishes.
223
+ // - We then remove the call from the repository map and clear per-call flags.
224
+ // - The Call.actionSource channel is no longer referenced and can be garbage-collected;
225
+ // we do not explicitly close it because callers use trySend(), which never suspends.
226
+ debugLog(TAG, "[repository] registerCall: Cleaning up call $callId")
227
+ removeCall(callId)
228
+ actionFlags.remove(callId)
178
229
  }
179
230
  }
180
231
 
@@ -187,63 +238,77 @@ class TelecomCallRepository(context: Context) : CallRepository(context) {
187
238
  ) {
188
239
  debugLog(
189
240
  TAG,
190
- "[repository] updateCall: Starting update - Name: $displayName, Address: $address, IsVideo: $isVideo"
241
+ "[repository] updateCall: Starting update - CallId: $callId, Name: $displayName, Address: $address, IsVideo: $isVideo"
191
242
  )
192
243
  super.updateCall(callId, displayName, address, isVideo, displayOptions)
193
244
  }
194
245
 
195
- private fun observeCallState(): Job {
196
- return currentCall
197
- .drop(1)
198
- .scan(Pair<Call?, Call>(null, currentCall.value)) { (_, prev), next ->
199
- Pair(prev, next)
200
- }
201
- .drop(1)
202
- .onEach { (previous, current) ->
203
- when {
204
- previous is Call.None && current is Call.Registered -> {
205
- _listener?.onCallRegistered(current.id, current.isIncoming())
206
- }
207
- previous is Call.Registered && current is Call.Registered -> {
208
- if (previous.isMuted != current.isMuted) {
209
- debugLog(
210
- TAG,
211
- "[repository] observeCallState: Mute changed: ${current.isMuted}"
212
- )
213
- _listener?.onMuteCallChanged(current.id, current.isMuted)
246
+ private fun observeCalls(): Job {
247
+ // Track previous state per call for diffing (only non-pending calls)
248
+ var previousCalls: Map<String, Call.Registered> = emptyMap()
249
+
250
+ return calls
251
+ .onEach { allCalls ->
252
+ // Filter out pending calls — they are not yet registered in Telecom
253
+ val currentCalls = allCalls.filter { (_, call) -> !call.isPending }
254
+
255
+ // Detect new calls
256
+ for ((callId, call) in currentCalls) {
257
+ val previous = previousCalls[callId]
258
+ if (previous == null) {
259
+ // New call added
260
+ _listener?.onCallRegistered(callId, call.isIncoming())
261
+ } else {
262
+ // Existing call changed
263
+ if (previous.isMuted != call.isMuted) {
264
+ debugLog(TAG, "[repository] observeCalls: Mute changed for $callId: ${call.isMuted}")
265
+ _listener?.onMuteCallChanged(callId, call.isMuted)
214
266
  }
215
- if (previous.currentCallEndpoint != current.currentCallEndpoint) {
216
- current.currentCallEndpoint?.let {
217
- _listener?.onCallEndpointChanged(current.id, it.name.toString())
267
+ if (previous.currentCallEndpoint != call.currentCallEndpoint) {
268
+ call.currentCallEndpoint?.let {
269
+ _listener?.onCallEndpointChanged(callId, it.name.toString())
218
270
  }
219
271
  }
220
272
  }
273
+ _listener?.onCallStateChanged(callId, call)
221
274
  }
222
- _listener?.onCallStateChanged(current)
275
+
276
+ // Detect removed calls
277
+ for ((callId, _) in previousCalls) {
278
+ if (!currentCalls.containsKey(callId)) {
279
+ _listener?.onCallStateChanged(callId, Call.None)
280
+ }
281
+ }
282
+
283
+ previousCalls = currentCalls
223
284
  }
224
285
  .launchIn(scope)
225
286
  }
226
287
 
227
288
  /** Collect the action source to handle client actions inside the call scope */
228
- private suspend fun CallControlScope.processCallActions(actionSource: Flow<CallAction>) {
289
+ private suspend fun CallControlScope.processCallActions(
290
+ callId: String,
291
+ flags: CallActionFlags,
292
+ actionSource: Flow<CallAction>
293
+ ) {
229
294
  actionSource.collect { action ->
230
- debugLog(TAG, "[repository] processCallActions: action: ${action::class.simpleName}")
295
+ debugLog(TAG, "[repository] processCallActions[$callId]: action: ${action::class.simpleName}")
231
296
  when (action) {
232
297
  is CallAction.Answer -> {
233
- doAnswer(action.isAudioCall)
298
+ doAnswer(callId, flags, action.isAudioCall)
234
299
  }
235
300
  is CallAction.Disconnect -> {
236
- doDisconnect(action)
301
+ doDisconnect(callId, flags, action)
237
302
  }
238
303
  is CallAction.SwitchAudioEndpoint -> {
239
- doSwitchEndpoint(action)
304
+ doSwitchEndpoint(callId, action)
240
305
  }
241
306
  is CallAction.TransferCall -> {
242
307
  debugLog(
243
308
  TAG,
244
- "[repository] processCallActions: Transfer to endpoint: ${action.endpointId}"
309
+ "[repository] processCallActions[$callId]: Transfer to endpoint: ${action.endpointId}"
245
310
  )
246
- val call = _currentCall.value as? Call.Registered
311
+ val call = _calls.value[callId]
247
312
  val endpoints =
248
313
  call?.availableCallEndpoints?.firstOrNull {
249
314
  it.identifier == action.endpointId
@@ -255,157 +320,141 @@ class TelecomCallRepository(context: Context) : CallRepository(context) {
255
320
  } else {
256
321
  Log.w(
257
322
  TAG,
258
- "[repository] processCallActions: Endpoint not found for transfer, ignoring"
323
+ "[repository] processCallActions[$callId]: Endpoint not found for transfer, ignoring"
259
324
  )
260
325
  }
261
326
  }
262
327
  CallAction.Hold -> {
263
328
  when (val result = setInactive()) {
264
329
  is CallControlResult.Success -> {
265
- onIsCallInactive()
330
+ onIsCallInactive(callId)()
266
331
  }
267
332
  is CallControlResult.Error -> {
268
333
  Log.e(
269
334
  TAG,
270
- "[repository] processCallActions: Hold action failed with error code: ${result.errorCode}"
335
+ "[repository] processCallActions[$callId]: Hold action failed with error code: ${result.errorCode}"
271
336
  )
272
- updateCurrentCall { copy(errorCode = result.errorCode) }
337
+ updateCallById(callId) { copy(errorCode = result.errorCode) }
273
338
  }
274
339
  }
275
340
  }
276
341
  CallAction.Activate -> {
277
342
  when (val result = setActive()) {
278
343
  is CallControlResult.Success -> {
279
- onIsCallActive()
344
+ onIsCallActive(callId)()
280
345
  }
281
346
  is CallControlResult.Error -> {
282
347
  Log.e(
283
348
  TAG,
284
- "[repository] processCallActions: Activate action failed with error code: ${result.errorCode}"
349
+ "[repository] processCallActions[$callId]: Activate action failed with error code: ${result.errorCode}"
285
350
  )
286
- updateCurrentCall { copy(errorCode = result.errorCode) }
351
+ updateCallById(callId) { copy(errorCode = result.errorCode) }
287
352
  }
288
353
  }
289
354
  }
290
355
  is CallAction.ToggleMute -> {
291
- // We cannot programmatically mute the telecom stack. Instead we just update
292
- // the state of the call and this will start/stop audio capturing.
293
- debugLog(TAG, "[repository] processCallActions: Toggling mute: ${action.isMute}")
294
- updateCurrentCall {
295
- val newMutedState = action.isMute
296
- copy(isMuted = newMutedState)
356
+ debugLog(TAG, "[repository] processCallActions[$callId]: Toggling mute: ${action.isMute}")
357
+ updateCallById(callId) {
358
+ copy(isMuted = action.isMute)
297
359
  }
298
360
  }
299
361
  }
300
362
  }
301
- debugLog(TAG, "[repository] processCallActions: Action collection ended")
363
+ debugLog(TAG, "[repository] processCallActions[$callId]: Action collection ended")
302
364
  }
303
365
 
304
366
 
305
- private suspend fun CallControlScope.doSwitchEndpoint(action: CallAction.SwitchAudioEndpoint) {
306
- debugLog(TAG, "[repository] doSwitchEndpoint: Switching to endpoint: ${action.endpointId}")
307
- if (_currentCall.value !is Call.Registered) {
308
- Log.w(TAG, "[repository] doSwitchEndpoint: Call not registered, ignoring")
367
+ private suspend fun CallControlScope.doSwitchEndpoint(callId: String, action: CallAction.SwitchAudioEndpoint) {
368
+ debugLog(TAG, "[repository] doSwitchEndpoint[$callId]: Switching to endpoint: ${action.endpointId}")
369
+ val call = _calls.value[callId]
370
+ if (call == null) {
371
+ Log.w(TAG, "[repository] doSwitchEndpoint[$callId]: Call not found, ignoring")
309
372
  return
310
373
  }
311
- // TODO once availableCallEndpoints is a state flow we can just get the value
312
- val endpoints = (_currentCall.value as Call.Registered).availableCallEndpoints
313
- // Switch to the given endpoint or fallback to the best possible one.
374
+ val endpoints = call.availableCallEndpoints
314
375
  val newEndpoint = endpoints.firstOrNull { it.identifier == action.endpointId }
315
376
 
316
377
  if (newEndpoint != null) {
317
378
  debugLog(
318
379
  TAG,
319
- "[repository] doSwitchEndpoint: Found endpoint: ${newEndpoint.name}, requesting change"
380
+ "[repository] doSwitchEndpoint[$callId]: Found endpoint: ${newEndpoint.name}, requesting change"
320
381
  )
321
382
  requestEndpointChange(newEndpoint).also {
322
- debugLog(TAG, "[repository] doSwitchEndpoint: Endpoint change result: $it")
383
+ debugLog(TAG, "[repository] doSwitchEndpoint[$callId]: Endpoint change result: $it")
323
384
  }
324
385
  } else {
325
- Log.w(TAG, "[repository] doSwitchEndpoint: Endpoint not found in available endpoints")
386
+ Log.w(TAG, "[repository] doSwitchEndpoint[$callId]: Endpoint not found in available endpoints")
326
387
  }
327
388
  }
328
389
 
329
- private suspend fun CallControlScope.doDisconnect(action: CallAction.Disconnect) {
330
- isSelfDisconnected = true
331
- debugLog(TAG, "[repository] doDisconnect: Disconnecting call with cause: ${action.cause}")
390
+ private suspend fun CallControlScope.doDisconnect(callId: String, flags: CallActionFlags, action: CallAction.Disconnect) {
391
+ flags.isSelfDisconnected.set(true)
392
+ debugLog(TAG, "[repository] doDisconnect[$callId]: Disconnecting call with cause: ${action.cause}")
332
393
  disconnect(action.cause)
333
- debugLog(TAG, "[repository] doDisconnect: Disconnect called, triggering onIsCallDisconnected")
334
- onIsCallDisconnected(action.cause)
394
+ debugLog(TAG, "[repository] doDisconnect[$callId]: Disconnect called, triggering onIsCallDisconnected")
395
+ onIsCallDisconnected(callId, flags)(action.cause)
335
396
  }
336
397
 
337
- private suspend fun CallControlScope.doAnswer(isAudioCall: Boolean) {
338
- isSelfAnswered = true
398
+ private suspend fun CallControlScope.doAnswer(callId: String, flags: CallActionFlags, isAudioCall: Boolean) {
399
+ flags.isSelfAnswered.set(true)
339
400
  val callType =
340
401
  if (isAudioCall) CallAttributesCompat.CALL_TYPE_AUDIO_CALL
341
402
  else CallAttributesCompat.CALL_TYPE_VIDEO_CALL
342
403
 
343
404
  when (val result = answer(callType)) {
344
405
  is CallControlResult.Success -> {
345
- onIsCallAnswered(callType)
406
+ onIsCallAnswered(callId, flags)(callType)
346
407
  }
347
408
  is CallControlResult.Error -> {
348
409
  Log.e(
349
410
  TAG,
350
- "[repository] doAnswer: Answer failed with error code: ${result.errorCode}"
411
+ "[repository] doAnswer[$callId]: Answer failed with error code: ${result.errorCode}"
351
412
  )
352
- isSelfAnswered = false
353
- updateCurrentCall {
354
- Call.Unregistered(
355
- id = id,
356
- callAttributes = callAttributes,
357
- disconnectCause = DisconnectCause(DisconnectCause.BUSY),
413
+ flags.isSelfAnswered.set(false)
414
+ val call = _calls.value[callId]
415
+ if (call != null) {
416
+ removeCall(callId)
417
+ _listener?.onIsCallDisconnected(
418
+ callId,
419
+ DisconnectCause(DisconnectCause.BUSY),
420
+ EventSource.APP
358
421
  )
359
422
  }
360
423
  }
361
424
  }
362
425
  }
363
426
 
364
- /**
365
- * Can the call be successfully answered?? TIP: We would check the connection/call state to see
366
- * if we can answer a call Example you may need to wait for another call to hold.
367
- */
368
- val onIsCallAnswered: suspend (type: Int) -> Unit = {
427
+ private fun onIsCallAnswered(callId: String, flags: CallActionFlags): suspend (type: Int) -> Unit = {
369
428
  debugLog(
370
429
  TAG,
371
- "[repository] onIsCallAnswered: Call answered, type: $it, isSelfAnswered: $isSelfAnswered"
430
+ "[repository] onIsCallAnswered[$callId]: Call answered, type: $it, isSelfAnswered: ${flags.isSelfAnswered.get()}"
372
431
  )
373
- updateCurrentCall { copy(isActive = true, isOnHold = false) }
432
+ updateCallById(callId) { copy(isActive = true, isOnHold = false) }
374
433
 
375
- val call = _currentCall.value
376
- val source = if (isSelfAnswered) EventSource.APP else EventSource.SYS
377
- if (call is Call.Registered) {
378
- _listener?.onIsCallAnswered(call.id, source)
434
+ val source = if (flags.isSelfAnswered.get()) EventSource.APP else EventSource.SYS
435
+ if (_calls.value.containsKey(callId)) {
436
+ _listener?.onIsCallAnswered(callId, source)
379
437
  }
380
- isSelfAnswered = false
381
- debugLog(TAG, "[repository] onIsCallAnswered: Call state updated to active")
438
+ flags.isSelfAnswered.set(false)
439
+ debugLog(TAG, "[repository] onIsCallAnswered[$callId]: Call state updated to active")
382
440
  }
383
441
 
384
- /** Can the call perform a disconnect */
385
- val onIsCallDisconnected: suspend (cause: DisconnectCause) -> Unit = {
442
+ private fun onIsCallDisconnected(callId: String, flags: CallActionFlags): suspend (cause: DisconnectCause) -> Unit = { cause ->
386
443
  debugLog(
387
444
  TAG,
388
- "[repository] onIsCallDisconnected: Call disconnected, cause: ${it.reason}, description: ${it.description}"
445
+ "[repository] onIsCallDisconnected[$callId]: Call disconnected, cause: ${cause.reason}, description: ${cause.description}"
389
446
  )
390
- val source = if (isSelfDisconnected) EventSource.APP else EventSource.SYS
391
- var callId: String? = null
392
- if (_currentCall.value is Call.Registered) {
393
- callId = (_currentCall.value as Call.Registered).id
394
- }
447
+ val source = if (flags.isSelfDisconnected.get()) EventSource.APP else EventSource.SYS
395
448
 
396
- updateCurrentCall { Call.Unregistered(id, callAttributes, it) }
397
- _listener?.onIsCallDisconnected(callId, it, source)
398
- isSelfDisconnected = false
399
- debugLog(TAG, "[repository] onIsCallDisconnected: Call state updated to Unregistered")
449
+ removeCall(callId)
450
+ _listener?.onIsCallDisconnected(callId, cause, source)
451
+ flags.isSelfDisconnected.set(false)
452
+ debugLog(TAG, "[repository] onIsCallDisconnected[$callId]: Call removed from map")
400
453
  }
401
454
 
402
- /**
403
- * Check is see if we can make the call active. Other calls and state might stop us from
404
- * activating the call
405
- */
406
- val onIsCallActive: suspend () -> Unit = {
407
- debugLog(TAG, "[repository] onIsCallActive: Call became active")
408
- updateCurrentCall {
455
+ private fun onIsCallActive(callId: String): suspend () -> Unit = {
456
+ debugLog(TAG, "[repository] onIsCallActive[$callId]: Call became active")
457
+ updateCallById(callId) {
409
458
  copy(
410
459
  errorCode = null,
411
460
  isActive = true,
@@ -413,23 +462,20 @@ class TelecomCallRepository(context: Context) : CallRepository(context) {
413
462
  )
414
463
  }
415
464
 
416
- val call = _currentCall.value
417
- if (call is Call.Registered) {
418
- _listener?.onIsCallActive(call.id)
465
+ if (_calls.value.containsKey(callId)) {
466
+ _listener?.onIsCallActive(callId)
419
467
  }
420
- debugLog(TAG, "[repository] onIsCallActive: Call state updated")
468
+ debugLog(TAG, "[repository] onIsCallActive[$callId]: Call state updated")
421
469
  }
422
470
 
423
- /** Check to see if we can make the call inactivate */
424
- val onIsCallInactive: suspend () -> Unit = {
425
- debugLog(TAG, "[repository] onIsCallInactive: Call became inactive (on hold)")
426
- updateCurrentCall { copy(errorCode = null, isOnHold = true) }
471
+ private fun onIsCallInactive(callId: String): suspend () -> Unit = {
472
+ debugLog(TAG, "[repository] onIsCallInactive[$callId]: Call became inactive (on hold)")
473
+ updateCallById(callId) { copy(errorCode = null, isOnHold = true) }
427
474
 
428
- val call = _currentCall.value
429
- if (call is Call.Registered) {
430
- _listener?.onIsCallInactive(call.id)
475
+ if (_calls.value.containsKey(callId)) {
476
+ _listener?.onIsCallInactive(callId)
431
477
  }
432
- debugLog(TAG, "[repository] onIsCallInactive: Call state updated to on hold")
478
+ debugLog(TAG, "[repository] onIsCallInactive[$callId]: Call state updated to on hold")
433
479
  }
434
480
 
435
481
  }