@capgo/capacitor-stream-call 0.0.5 → 0.0.18

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.
@@ -6,6 +6,8 @@ import android.app.Application
6
6
  import android.app.KeyguardManager
7
7
  import android.content.Context
8
8
  import android.graphics.Color
9
+ import android.media.RingtoneManager
10
+ import android.net.Uri
9
11
  import android.os.Bundle
10
12
  import android.os.Handler
11
13
  import android.os.Looper
@@ -20,13 +22,11 @@ import com.getcapacitor.Plugin
20
22
  import com.getcapacitor.PluginCall
21
23
  import com.getcapacitor.PluginMethod
22
24
  import com.getcapacitor.annotation.CapacitorPlugin
23
- import io.getstream.android.push.firebase.FirebasePushDeviceGenerator
24
25
  import io.getstream.android.push.permissions.ActivityLifecycleCallbacks
25
26
  import io.getstream.video.android.core.Call
26
27
  import io.getstream.video.android.core.GEO
27
28
  import io.getstream.video.android.core.StreamVideo
28
29
  import io.getstream.video.android.core.StreamVideoBuilder
29
- import io.getstream.video.android.core.model.RejectReason
30
30
  import io.getstream.video.android.core.notifications.NotificationConfig
31
31
  import io.getstream.video.android.core.notifications.NotificationHandler
32
32
  import io.getstream.video.android.core.sounds.emptyRingingConfig
@@ -35,16 +35,21 @@ import io.getstream.video.android.model.StreamCallId
35
35
  import io.getstream.video.android.model.User
36
36
  import io.getstream.video.android.model.streamCallId
37
37
  import kotlinx.coroutines.DelicateCoroutinesApi
38
- import kotlinx.coroutines.flow.Flow
39
38
  import kotlinx.coroutines.launch
40
- import org.openapitools.client.models.CallAcceptedEvent
41
- import org.openapitools.client.models.CallEndedEvent
42
- import org.openapitools.client.models.CallMissedEvent
43
- import org.openapitools.client.models.CallRejectedEvent
44
- import org.openapitools.client.models.CallSessionEndedEvent
45
- import org.openapitools.client.models.VideoEvent
46
39
  import io.getstream.video.android.model.Device
47
- import kotlinx.coroutines.flow.first
40
+ import kotlinx.coroutines.tasks.await
41
+ import com.google.firebase.messaging.FirebaseMessaging
42
+ import io.getstream.android.push.PushProvider
43
+ import io.getstream.android.push.firebase.FirebasePushDeviceGenerator
44
+ import io.getstream.android.video.generated.models.CallAcceptedEvent
45
+ import io.getstream.android.video.generated.models.CallCreatedEvent
46
+ import io.getstream.android.video.generated.models.CallEndedEvent
47
+ import io.getstream.android.video.generated.models.CallMissedEvent
48
+ import io.getstream.android.video.generated.models.CallRejectedEvent
49
+ import io.getstream.android.video.generated.models.CallSessionEndedEvent
50
+ import io.getstream.android.video.generated.models.CallSessionStartedEvent
51
+ import io.getstream.android.video.generated.models.VideoEvent
52
+ import io.getstream.video.android.core.sounds.RingingConfig
48
53
 
49
54
  // I am not a religious pearson, but at this point, I am not sure even god himself would understand this code
50
55
  // It's a spaghetti-like, tangled, unreadable mess and frankly, I am deeply sorry for the code crimes commited in the Android impl
@@ -63,6 +68,12 @@ public class StreamCallPlugin : Plugin() {
63
68
  private var savedActivity: Activity? = null
64
69
  private var savedActivityPaused = false
65
70
  private var savedCallsToEndOnResume = mutableListOf<Call>()
71
+ private val callStates: MutableMap<String, LocalCallState> = mutableMapOf()
72
+
73
+ // Store current call info
74
+ private var currentCallId: String = ""
75
+ private var currentCallType: String = ""
76
+ private var currentCallState: String = ""
66
77
 
67
78
  private enum class State {
68
79
  NOT_INITIALIZED,
@@ -70,6 +81,11 @@ public class StreamCallPlugin : Plugin() {
70
81
  INITIALIZED
71
82
  }
72
83
 
84
+ public fun incomingOnlyRingingConfig(): RingingConfig = object : RingingConfig {
85
+ override val incomingCallSoundUri: Uri? = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
86
+ override val outgoingCallSoundUri: Uri? = null
87
+ }
88
+
73
89
  private fun runOnMainThread(action: () -> Unit) {
74
90
  mainHandler.post { action() }
75
91
  }
@@ -210,12 +226,8 @@ public class StreamCallPlugin : Plugin() {
210
226
  // Stop ringtone
211
227
  ringtonePlayer?.stopRinging()
212
228
 
213
- // Notify that call has ended
214
- val data = JSObject().apply {
215
- put("callId", call.id)
216
- put("state", "rejected")
217
- }
218
- notifyListeners("callEvent", data)
229
+ // Notify that call has ended using our helper
230
+ updateCallStatusAndNotify(call.id, "rejected")
219
231
 
220
232
  hideIncomingCall()
221
233
  } catch (e: Exception) {
@@ -249,14 +261,6 @@ public class StreamCallPlugin : Plugin() {
249
261
  }
250
262
  }
251
263
 
252
- // private fun remoteIncomingCallNotif() {
253
- // CallService.removeIncomingCall(
254
- // context,
255
- // StreamCallId.fromCallCid(call.cid),
256
- // StreamVideo.instance().state.callConfigRegistry.get(call.type),
257
- // )
258
- // }
259
-
260
264
  private fun setupViews() {
261
265
  val context = context
262
266
  val parent = bridge?.webView?.parent as? ViewGroup ?: return
@@ -342,7 +346,7 @@ public class StreamCallPlugin : Plugin() {
342
346
  SecureUserRepository.getInstance(context).save(credentials)
343
347
 
344
348
  // Initialize Stream Video with new credentials
345
- if (!hadSavedCredentials || (savedCredentials!!.user.id !== userId)) {
349
+ if (!hadSavedCredentials || (savedCredentials!!.user.id != userId)) {
346
350
  initializeStreamVideo()
347
351
  }
348
352
 
@@ -467,16 +471,17 @@ public class StreamCallPlugin : Plugin() {
467
471
  )
468
472
 
469
473
  val notificationConfig = NotificationConfig(
470
- pushDeviceGenerators = listOf(FirebasePushDeviceGenerator(
474
+ pushDeviceGenerators = listOf(
475
+ FirebasePushDeviceGenerator(
471
476
  providerName = "firebase",
472
477
  context = contextToUse
473
- )),
478
+ )
479
+ ),
474
480
  requestPermissionOnAppLaunch = { true },
475
481
  notificationHandler = notificationHandler,
476
482
  )
477
483
 
478
- val soundsConfig = emptyRingingConfig()
479
- soundsConfig.incomingCallSoundUri
484
+ val soundsConfig = incomingOnlyRingingConfig()
480
485
  // Initialize StreamVideo client
481
486
  streamVideoClient = StreamVideoBuilder(
482
487
  context = contextToUse,
@@ -560,89 +565,109 @@ public class StreamCallPlugin : Plugin() {
560
565
  client.subscribe { event: VideoEvent ->
561
566
  android.util.Log.v("StreamCallPlugin", "Received an event ${event.getEventType()} $event")
562
567
  when (event) {
563
- is CallEndedEvent -> {
564
- runOnMainThread {
565
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallEndedEvent for call ${event.call.cid}")
566
- overlayView?.setContent {
567
- CallOverlayView(
568
- context = context,
569
- streamVideo = streamVideoClient,
570
- call = null
571
- )
572
- }
573
- overlayView?.isVisible = false
574
- // Stop ringtone if it's still playing
575
- ringtonePlayer?.stopRinging()
576
- // Hide incoming call view if visible
577
- incomingCallView?.isVisible = false
578
- }
579
- val data = JSObject().apply {
580
- put("callId", event.call.cid)
581
- put("state", "left")
582
- }
583
- notifyListeners("callEvent", data)
568
+ // Handle CallCreatedEvent differently - only log it but don't try to access members yet
569
+ is CallCreatedEvent -> {
570
+ val callCid = event.callCid
571
+ android.util.Log.d("StreamCallPlugin", "Call created: $callCid")
572
+
573
+ // let's get the members
574
+ val callParticipants = event.members.filter{ it.user.id != this@StreamCallPlugin.streamVideoClient?.userId }.map { it.user.id }
575
+ android.util.Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} participants")
576
+
577
+ // Start tracking this call now that we have the member list
578
+ startCallTimeoutMonitor(callCid, callParticipants)
579
+
580
+ // Use direction from event if available
581
+ val callType = callCid.split(":").firstOrNull() ?: "default"
582
+ updateCallStatusAndNotify(callCid, "created")
584
583
  }
585
- is CallSessionEndedEvent -> {
586
- runOnMainThread {
587
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallSessionEndedEvent for call ${event.call.cid}")
588
- overlayView?.setContent {
589
- CallOverlayView(
590
- context = context,
591
- streamVideo = streamVideoClient,
592
- call = null
593
- )
594
- }
595
- overlayView?.isVisible = false
596
- // Stop ringtone if it's still playing
597
- ringtonePlayer?.stopRinging()
598
- // Hide incoming call view if visible
599
- incomingCallView?.isVisible = false
600
- }
601
- val data = JSObject().apply {
602
- put("callId", event.call.cid)
603
- put("state", "left")
604
- }
605
- notifyListeners("callEvent", data)
584
+ // Add handler for CallSessionStartedEvent which contains participant information
585
+ is CallSessionStartedEvent -> {
586
+ val callCid = event.callCid
587
+ updateCallStatusAndNotify(callCid, "session_started")
606
588
  }
589
+
607
590
  is CallRejectedEvent -> {
608
- runOnMainThread {
609
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallRejectedEvent for call ${event.call.cid}")
610
- overlayView?.setContent {
611
- CallOverlayView(
612
- context = context,
613
- streamVideo = streamVideoClient,
614
- call = null
615
- )
616
- }
617
- overlayView?.isVisible = false
618
-
619
- val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
620
- if (keyguardManager.isKeyguardLocked) {
621
- android.util.Log.d("StreamCallPlugin", "Stop ringing and move to background")
622
- this@StreamCallPlugin.ringtonePlayer?.stopRinging()
623
- moveAllActivitiesToBackgroundOrKill(context)
624
- }
625
- }
626
- val data = JSObject().apply {
627
- put("callId", event.call.cid)
628
- put("state", "rejected")
591
+ val userId = event.user.id
592
+ val callCid = event.callCid
593
+
594
+ // Update call state
595
+ callStates[callCid]?.let { callState ->
596
+ callState.participantResponses[userId] = "rejected"
629
597
  }
630
- notifyListeners("callEvent", data)
598
+
599
+ updateCallStatusAndNotify(callCid, "rejected", userId)
600
+
601
+ // Check if all participants have responded
602
+ checkAllParticipantsResponded(callCid)
631
603
  }
604
+
632
605
  is CallMissedEvent -> {
606
+ val userId = event.user.id
607
+ val callCid = event.callCid
608
+
609
+ // Update call state
610
+ callStates[callCid]?.let { callState ->
611
+ callState.participantResponses[userId] = "missed"
612
+ }
613
+
633
614
  val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
634
615
  if (keyguardManager.isKeyguardLocked) {
635
616
  android.util.Log.d("StreamCallPlugin", "Stop ringing and move to background")
636
617
  this.ringtonePlayer?.stopRinging()
637
618
  moveAllActivitiesToBackgroundOrKill(context)
638
619
  }
620
+
621
+ updateCallStatusAndNotify(callCid, "missed", userId)
622
+
623
+ // Check if all participants have responded
624
+ checkAllParticipantsResponded(callCid)
625
+ }
626
+
627
+ is CallAcceptedEvent -> {
628
+ val userId = event.user.id
629
+ val callCid = event.callCid
630
+
631
+ // Update call state
632
+ callStates[callCid]?.let { callState ->
633
+ callState.participantResponses[userId] = "accepted"
634
+
635
+ // Since someone accepted, cancel the timeout timer
636
+ android.util.Log.d("StreamCallPlugin", "Call accepted by $userId, canceling timeout timer for $callCid")
637
+ callState.timer?.removeCallbacksAndMessages(null)
638
+ callState.timer = null
639
+ }
640
+
641
+ updateCallStatusAndNotify(callCid, "accepted", userId)
642
+ }
643
+
644
+ is CallEndedEvent -> {
645
+ runOnMainThread {
646
+ android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallEndedEvent for call ${event.callCid}")
647
+ // Clean up call resources
648
+ val callCid = event.callCid
649
+ cleanupCall(callCid)
650
+ }
651
+ updateCallStatusAndNotify(event.callCid, "left")
652
+ }
653
+
654
+ is CallSessionEndedEvent -> {
655
+ runOnMainThread {
656
+ android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallSessionEndedEvent for call ${event.callCid}. Test session: ${event.call.session?.endedAt}")
657
+ // Clean up call resources
658
+ val callCid = event.callCid
659
+ cleanupCall(callCid)
660
+ }
661
+ updateCallStatusAndNotify(event.callCid, "left")
662
+ }
663
+
664
+ else -> {
665
+ updateCallStatusAndNotify(
666
+ streamVideoClient?.state?.activeCall?.value?.cid ?: "",
667
+ event.getEventType()
668
+ )
639
669
  }
640
670
  }
641
- val data = JSObject().apply {
642
- put("callId", streamVideoClient?.state?.activeCall?.value?.cid)
643
- put("state", event.getEventType())
644
- }
645
- notifyListeners("callEvent", data)
646
671
  }
647
672
 
648
673
  // Add call state subscription using collect
@@ -657,19 +682,11 @@ public class StreamCallPlugin : Plugin() {
657
682
  android.util.Log.d("StreamCallPlugin", "- All participants: ${state.participants}")
658
683
  android.util.Log.d("StreamCallPlugin", "- Remote participants: ${state.remoteParticipants}")
659
684
 
660
- // Notify that a call has started
661
- val data = JSObject().apply {
662
- put("callId", call.cid)
663
- put("state", "joined")
664
- }
665
- notifyListeners("callEvent", data)
685
+ // Notify that a call has started using our helper
686
+ updateCallStatusAndNotify(call.cid, "joined")
666
687
  } ?: run {
667
- // Notify that call has ended
668
- val data = JSObject().apply {
669
- put("callId", "")
670
- put("state", "left")
671
- }
672
- notifyListeners("callEvent", data)
688
+ // Notify that call has ended using our helper
689
+ updateCallStatusAndNotify("", "left")
673
690
  }
674
691
  }
675
692
  }
@@ -743,12 +760,8 @@ public class StreamCallPlugin : Plugin() {
743
760
  // Join the call without affecting others
744
761
  call.accept()
745
762
 
746
- // Notify that call has started
747
- val data = JSObject().apply {
748
- put("callId", call.id)
749
- put("state", "joined")
750
- }
751
- notifyListeners("callEvent", data)
763
+ // Notify that call has started using helper
764
+ updateCallStatusAndNotify(call.id, "joined")
752
765
 
753
766
  // Show overlay view with the active call
754
767
  runOnMainThread {
@@ -871,7 +884,6 @@ public class StreamCallPlugin : Plugin() {
871
884
  val callId = call.id
872
885
  android.util.Log.d("StreamCallPlugin", "Attempting to end call $callId")
873
886
  call.leave()
874
- call.reject(reason = RejectReason.Cancel)
875
887
 
876
888
  // Capture context from the overlayView
877
889
  val currentContext = overlayView?.context ?: this.savedContext
@@ -924,12 +936,8 @@ public class StreamCallPlugin : Plugin() {
924
936
  incomingCallView?.isVisible = false
925
937
  }
926
938
 
927
- // Notify that call has ended
928
- val data = JSObject().apply {
929
- put("callId", callId)
930
- put("state", "left")
931
- }
932
- notifyListeners("callEvent", data)
939
+ // Notify that call has ended using helper
940
+ updateCallStatusAndNotify(callId, "left")
933
941
  }
934
942
 
935
943
  @OptIn(DelicateCoroutinesApi::class)
@@ -1015,6 +1023,7 @@ public class StreamCallPlugin : Plugin() {
1015
1023
  val callType = call.getString("type") ?: "default"
1016
1024
  val shouldRing = call.getBoolean("ring") ?: true
1017
1025
  val callId = java.util.UUID.randomUUID().toString()
1026
+ val team = call.getString("team");
1018
1027
 
1019
1028
  android.util.Log.d("StreamCallPlugin", "Creating call:")
1020
1029
  android.util.Log.d("StreamCallPlugin", "- Call ID: $callId")
@@ -1027,70 +1036,29 @@ public class StreamCallPlugin : Plugin() {
1027
1036
  try {
1028
1037
  // Create the call object
1029
1038
  val streamCall = streamVideoClient?.call(type = callType, id = callId)
1030
-
1031
- // Track participants responses
1032
- val participantResponses = mutableMapOf<String, String>()
1033
- val totalParticipants = userIds.size
1034
-
1035
- // Subscribe to call events for this specific call
1036
- streamCall?.subscribe { event ->
1037
- when (event) {
1038
- is CallRejectedEvent -> {
1039
- val userId = event.user.id
1040
- android.util.Log.d("StreamCallPlugin", "Call was rejected by user: $userId")
1041
- participantResponses[userId] = "rejected"
1042
-
1043
- val data = JSObject().apply {
1044
- put("callId", callId)
1045
- put("state", "rejected")
1046
- put("userId", userId)
1047
- }
1048
- notifyListeners("callEvent", data)
1049
-
1050
- // Check if all participants have rejected or missed
1051
- checkAllParticipantsResponded(participantResponses, totalParticipants, streamCall)
1052
- }
1053
- is CallMissedEvent -> {
1054
- val userId = event.user.id
1055
- android.util.Log.d("StreamCallPlugin", "Call was missed by user: $userId")
1056
- participantResponses[userId] = "missed"
1057
-
1058
- val data = JSObject().apply {
1059
- put("callId", callId)
1060
- put("state", "missed")
1061
- put("userId", userId)
1062
- }
1063
- notifyListeners("callEvent", data)
1064
-
1065
- // Check if all participants have rejected or missed
1066
- checkAllParticipantsResponded(participantResponses, totalParticipants, streamCall)
1067
- }
1068
- is CallAcceptedEvent -> {
1069
- val userId = event.user.id
1070
- android.util.Log.d("StreamCallPlugin", "Call was accepted by user: $userId")
1071
- participantResponses[userId] = "accepted"
1072
-
1073
- val data = JSObject().apply {
1074
- put("callId", callId)
1075
- put("state", "accepted")
1076
- put("userId", userId)
1077
- }
1078
- notifyListeners("callEvent", data)
1079
- }
1080
- }
1081
- }
1082
-
1039
+
1040
+ // Note: We no longer start tracking here - we'll wait for CallSessionStartedEvent
1041
+ // instead, which contains the actual participant list
1042
+
1083
1043
  android.util.Log.d("StreamCallPlugin", "Creating call with members...")
1084
1044
  // Create the call with all members
1085
- streamCall?.create(
1045
+ val createResult = streamCall?.create(
1086
1046
  memberIds = userIds + selfUserId,
1087
1047
  custom = emptyMap(),
1088
- ring = shouldRing
1048
+ ring = shouldRing,
1049
+ team = team,
1089
1050
  )
1051
+
1052
+ if (createResult?.isFailure == true) {
1053
+ throw (createResult.errorOrNull() ?: RuntimeException("Unknown error creating call")) as Throwable
1054
+ }
1090
1055
 
1091
1056
  android.util.Log.d("StreamCallPlugin", "Setting overlay visible for outgoing call $callId")
1092
1057
  // Show overlay view
1093
1058
  activity?.runOnUiThread {
1059
+ streamCall?.microphone?.setEnabled(true)
1060
+ streamCall?.camera?.setEnabled(true)
1061
+
1094
1062
  overlayView?.setContent {
1095
1063
  CallOverlayView(
1096
1064
  context = context,
@@ -1115,104 +1083,214 @@ public class StreamCallPlugin : Plugin() {
1115
1083
  }
1116
1084
  }
1117
1085
 
1118
- @OptIn(DelicateCoroutinesApi::class)
1119
- private fun checkAllParticipantsResponded(
1120
- participantResponses: Map<String, String>,
1121
- totalParticipants: Int,
1122
- streamCall: Call
1123
- ) {
1124
- kotlinx.coroutines.GlobalScope.launch {
1125
- try {
1126
- val allResponded = participantResponses.size == totalParticipants
1127
- val allRejectedOrMissed = participantResponses.values.all { it == "rejected" || it == "missed" }
1086
+ private fun startCallTimeoutMonitor(callCid: String, memberIds: List<String>) {
1087
+ val callState = LocalCallState(members = memberIds)
1088
+
1089
+ val handler = Handler(Looper.getMainLooper())
1090
+ val timeoutRunnable = object : Runnable {
1091
+ override fun run() {
1092
+ checkCallTimeout(callCid)
1093
+ handler.postDelayed(this, 1000)
1094
+ }
1095
+ }
1096
+
1097
+ handler.postDelayed(timeoutRunnable, 1000)
1098
+ callState.timer = handler
1099
+
1100
+ callStates[callCid] = callState
1101
+
1102
+ android.util.Log.d("StreamCallPlugin", "Started timeout monitor for call $callCid with ${memberIds.size} members")
1103
+ }
1104
+
1105
+ private fun checkCallTimeout(callCid: String) {
1106
+ val callState = callStates[callCid] ?: return
1107
+
1108
+ val now = System.currentTimeMillis()
1109
+ val elapsedSeconds = (now - callState.createdAt) / 1000
1110
+
1111
+ if (elapsedSeconds >= 30) {
1112
+ android.util.Log.d("StreamCallPlugin", "Call $callCid has timed out after $elapsedSeconds seconds")
1113
+
1114
+ val hasAccepted = callState.participantResponses.values.any { it == "accepted" }
1115
+
1116
+ if (!hasAccepted) {
1117
+ android.util.Log.d("StreamCallPlugin", "No one accepted call $callCid, marking all non-responders as missed")
1128
1118
 
1129
- if (allResponded && allRejectedOrMissed) {
1130
- android.util.Log.d("StreamCallPlugin", "All participants have rejected or missed the call")
1131
- streamCall.leave()
1132
- activity?.runOnUiThread {
1133
- overlayView?.setContent {
1134
- CallOverlayView(
1135
- context = context,
1136
- streamVideo = streamVideoClient,
1137
- call = null
1138
- )
1139
- }
1140
- overlayView?.isVisible = false
1119
+ // First, remove the timer to prevent further callbacks
1120
+ callState.timer?.removeCallbacksAndMessages(null)
1121
+ callState.timer = null
1122
+
1123
+ callState.members.forEach { memberId ->
1124
+ if (memberId !in callState.participantResponses) {
1125
+ callState.participantResponses[memberId] = "missed"
1126
+
1127
+ updateCallStatusAndNotify(callCid, "missed", memberId)
1141
1128
  }
1129
+ }
1130
+
1131
+ val callIdParts = callCid.split(":")
1132
+ if (callIdParts.size >= 2) {
1133
+ val callType = callIdParts[0]
1134
+ val callId = callIdParts[1]
1142
1135
 
1143
- val data = JSObject().apply {
1144
- put("callId", streamCall.id)
1145
- put("state", "ended")
1146
- put("reason", "all_rejected_or_missed")
1136
+ streamVideoClient?.call(type = callType, id = callId)?.let { call ->
1137
+ kotlinx.coroutines.GlobalScope.launch {
1138
+ try {
1139
+ // Use endCallRaw instead of manual cleanup
1140
+ endCallRaw(call)
1141
+
1142
+ // Clean up state - we don't need to do this in endCallRaw because we already did it here
1143
+ callStates.remove(callCid)
1144
+
1145
+ // Notify that call has ended using helper
1146
+ updateCallStatusAndNotify(callCid, "ended", null, "timeout")
1147
+ } catch (e: Exception) {
1148
+ android.util.Log.e("StreamCallPlugin", "Error ending timed out call", e)
1149
+ }
1150
+ }
1147
1151
  }
1148
- notifyListeners("callEvent", data)
1149
1152
  }
1150
- } catch (e: Exception) {
1151
- android.util.Log.e("StreamCallPlugin", "Error checking participant responses: ${e.message}")
1152
1153
  }
1153
1154
  }
1154
1155
  }
1155
1156
 
1156
- private suspend fun magicDeviceDelete(streamVideoClient: StreamVideo) {
1157
- try {
1158
- android.util.Log.d("StreamCallPlugin", "Starting magicDeviceDelete reflection operation")
1159
-
1160
- // Get the streamNotificationManager field from StreamVideo
1161
- val streamVideoClass = streamVideoClient.javaClass
1162
- val notificationManagerField = streamVideoClass.getDeclaredField("streamNotificationManager")
1163
- notificationManagerField.isAccessible = true
1164
- val notificationManager = notificationManagerField.get(streamVideoClient)
1165
-
1166
- if (notificationManager == null) {
1167
- android.util.Log.e("StreamCallPlugin", "streamNotificationManager is null")
1168
- return
1169
- }
1170
-
1171
- android.util.Log.d("StreamCallPlugin", "Successfully accessed streamNotificationManager")
1172
-
1173
- // Get deviceTokenStorage from notification manager
1174
- val notificationManagerClass = notificationManager.javaClass
1175
- val deviceTokenStorageField = notificationManagerClass.getDeclaredField("deviceTokenStorage")
1176
- deviceTokenStorageField.isAccessible = true
1177
- val deviceTokenStorage = deviceTokenStorageField.get(notificationManager)
1178
-
1179
- if (deviceTokenStorage == null) {
1180
- android.util.Log.e("StreamCallPlugin", "deviceTokenStorage is null")
1181
- return
1182
- }
1183
-
1184
- android.util.Log.d("StreamCallPlugin", "Successfully accessed deviceTokenStorage")
1185
-
1186
- // Access the DeviceTokenStorage object dynamically without hardcoding class
1187
- val deviceTokenStorageClass = deviceTokenStorage.javaClass
1188
-
1189
- // Get the userDevice Flow from deviceTokenStorage
1190
- val userDeviceField = deviceTokenStorageClass.getDeclaredField("userDevice")
1191
- userDeviceField.isAccessible = true
1192
- val userDeviceFlow = userDeviceField.get(deviceTokenStorage)
1157
+ private fun cleanupCall(callCid: String) {
1158
+ // Get the call state
1159
+ val callState = callStates[callCid]
1160
+
1161
+ if (callState != null) {
1162
+ // Ensure timer is properly canceled
1163
+ android.util.Log.d("StreamCallPlugin", "Stopping timer for call: $callCid")
1164
+ callState.timer?.removeCallbacksAndMessages(null)
1165
+ callState.timer = null
1166
+ }
1167
+
1168
+ // Remove from callStates
1169
+ callStates.remove(callCid)
1170
+
1171
+ // Hide UI elements directly without setting content
1172
+ runOnMainThread {
1173
+ android.util.Log.d("StreamCallPlugin", "Hiding UI elements for call $callCid (one-time cleanup)")
1174
+ overlayView?.isVisible = false
1175
+ ringtonePlayer?.stopRinging()
1176
+ incomingCallView?.isVisible = false
1177
+ }
1178
+
1179
+ android.util.Log.d("StreamCallPlugin", "Cleaned up resources for ended call: $callCid")
1180
+ }
1181
+
1182
+ private fun checkAllParticipantsResponded(callCid: String) {
1183
+ val callState = callStates[callCid] ?: return
1184
+
1185
+ val totalParticipants = callState.members.size
1186
+ val responseCount = callState.participantResponses.size
1187
+
1188
+ android.util.Log.d("StreamCallPlugin", "Checking responses for call $callCid: $responseCount / $totalParticipants")
1189
+
1190
+ val allResponded = responseCount >= totalParticipants
1191
+ val allRejectedOrMissed = allResponded &&
1192
+ callState.participantResponses.values.all { it == "rejected" || it == "missed" }
1193
+
1194
+ if (allResponded && allRejectedOrMissed) {
1195
+ android.util.Log.d("StreamCallPlugin", "All participants have rejected or missed the call $callCid")
1193
1196
 
1194
- if (userDeviceFlow == null) {
1195
- android.util.Log.e("StreamCallPlugin", "userDevice Flow is null")
1196
- return
1197
- }
1197
+ // Cancel the timer immediately to prevent further callbacks
1198
+ callState.timer?.removeCallbacksAndMessages(null)
1199
+ callState.timer = null
1198
1200
 
1199
- android.util.Log.d("StreamCallPlugin", "Successfully accessed userDevice Flow: $userDeviceFlow")
1200
-
1201
- val castedUserDeviceFlow = userDeviceFlow as Flow<Device?>
1202
- try {
1203
- castedUserDeviceFlow.first {
1204
- if (it == null) {
1205
- android.util.Log.d("StreamCallPlugin", "Device is null. Nothing to remove")
1206
- return@first true;
1201
+ // End the call using endCallRaw
1202
+ val callIdParts = callCid.split(":")
1203
+ if (callIdParts.size >= 2) {
1204
+ val callType = callIdParts[0]
1205
+ val callId = callIdParts[1]
1206
+
1207
+ streamVideoClient?.call(type = callType, id = callId)?.let { call ->
1208
+ kotlinx.coroutines.GlobalScope.launch {
1209
+ try {
1210
+ // Use endCallRaw instead of manual cleanup
1211
+ endCallRaw(call)
1212
+
1213
+ // Clean up state - we don't need to do this in endCallRaw because we already did it here
1214
+ callStates.remove(callCid)
1215
+
1216
+ // Notify that call has ended using helper
1217
+ updateCallStatusAndNotify(callCid, "ended", null, "all_rejected_or_missed")
1218
+ } catch (e: Exception) {
1219
+ android.util.Log.e("StreamCallPlugin", "Error ending call after all rejected/missed", e)
1220
+ }
1207
1221
  }
1208
- streamVideoClient.deleteDevice(it)
1209
- return@first true;
1210
1222
  }
1211
- } catch (e: Throwable) {
1212
- android.util.Log.e("StreamCallPlugin", "Cannot collect flow in magicDeviceDelete", e)
1223
+ }
1224
+ }
1225
+ }
1226
+
1227
+ private suspend fun magicDeviceDelete(streamVideoClient: StreamVideo) {
1228
+ try {
1229
+ android.util.Log.d("StreamCallPlugin", "Starting magicDeviceDelete operation")
1230
+
1231
+ FirebaseMessaging.getInstance().token.await()?.let {
1232
+ android.util.Log.d("StreamCallPlugin", "Found firebase token")
1233
+ val device = Device(
1234
+ id = it,
1235
+ pushProvider = PushProvider.FIREBASE.key,
1236
+ pushProviderName = "firebase",
1237
+ )
1238
+
1239
+ streamVideoClient.deleteDevice(device)
1213
1240
  }
1214
1241
  } catch (e: Exception) {
1215
1242
  android.util.Log.e("StreamCallPlugin", "Error in magicDeviceDelete", e)
1216
1243
  }
1217
1244
  }
1245
+
1246
+ @PluginMethod
1247
+ fun getCallStatus(call: PluginCall) {
1248
+ // If not in a call, reject
1249
+ if (currentCallId.isEmpty() || currentCallState == "left") {
1250
+ call.reject("Not in a call")
1251
+ return
1252
+ }
1253
+
1254
+ val result = JSObject()
1255
+ result.put("callId", currentCallId)
1256
+ result.put("state", currentCallState)
1257
+
1258
+ // No additional fields to ensure compatibility with CallEvent interface
1259
+
1260
+ call.resolve(result)
1261
+ }
1262
+
1263
+ // Helper method to update call status and notify listeners
1264
+ private fun updateCallStatusAndNotify(callId: String, state: String, userId: String? = null, reason: String? = null) {
1265
+ // Update stored call info
1266
+ currentCallId = callId
1267
+ currentCallState = state
1268
+
1269
+ // Get call type from call ID if available
1270
+ if (callId.contains(":")) {
1271
+ currentCallType = callId.split(":").firstOrNull() ?: ""
1272
+ }
1273
+
1274
+ // Create data object with only the fields in the CallEvent interface
1275
+ val data = JSObject().apply {
1276
+ put("callId", callId)
1277
+ put("state", state)
1278
+ userId?.let {
1279
+ put("userId", it)
1280
+ }
1281
+ reason?.let {
1282
+ put("reason", it)
1283
+ }
1284
+ }
1285
+
1286
+ // Notify listeners
1287
+ notifyListeners("callEvent", data)
1288
+ }
1289
+
1290
+ data class LocalCallState(
1291
+ val members: List<String>,
1292
+ val participantResponses: MutableMap<String, String> = mutableMapOf(),
1293
+ val createdAt: Long = System.currentTimeMillis(),
1294
+ var timer: Handler? = null
1295
+ )
1218
1296
  }