@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.
- package/CapgoCapacitorStreamCall.podspec +2 -2
- package/Package.swift +1 -1
- package/README.md +265 -11
- package/android/build.gradle +22 -4
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomNotificationHandler.kt +2 -2
- package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +338 -260
- package/dist/docs.json +761 -12
- package/dist/esm/definitions.d.ts +57 -23
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -1
- package/dist/esm/web.js +58 -22
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +58 -22
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +58 -22
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +151 -142
- package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +3 -3
- package/package.json +2 -2
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1197
|
+
// Cancel the timer immediately to prevent further callbacks
|
|
1198
|
+
callState.timer?.removeCallbacksAndMessages(null)
|
|
1199
|
+
callState.timer = null
|
|
1198
1200
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
-
}
|
|
1212
|
-
|
|
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
|
}
|