@capgo/capacitor-stream-call 0.0.26 → 0.0.28

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.
@@ -48,8 +48,21 @@ import io.getstream.android.video.generated.models.CallRejectedEvent
48
48
  import io.getstream.android.video.generated.models.CallRingEvent
49
49
  import io.getstream.android.video.generated.models.CallSessionEndedEvent
50
50
  import io.getstream.android.video.generated.models.CallSessionStartedEvent
51
- import io.getstream.android.video.generated.models.VideoEvent
52
51
  import io.getstream.video.android.core.sounds.RingingConfig
52
+ import kotlinx.coroutines.CoroutineScope
53
+ import kotlinx.coroutines.Dispatchers
54
+ import android.Manifest
55
+ import android.content.pm.PackageManager
56
+ import androidx.core.app.ActivityCompat
57
+ import androidx.core.content.ContextCompat
58
+ import io.getstream.android.video.generated.models.VideoEvent
59
+ import io.getstream.video.android.compose.theme.VideoTheme
60
+ import io.getstream.video.android.compose.ui.components.call.activecall.CallContent
61
+ import androidx.compose.runtime.collectAsState
62
+ import io.getstream.video.android.core.CameraDirection
63
+ import android.content.BroadcastReceiver
64
+ import android.content.Intent
65
+ import android.content.IntentFilter
53
66
 
54
67
  // I am not a religious pearson, but at this point, I am not sure even god himself would understand this code
55
68
  // It's a spaghetti-like, tangled, unreadable mess and frankly, I am deeply sorry for the code crimes commited in the Android impl
@@ -58,7 +71,6 @@ public class StreamCallPlugin : Plugin() {
58
71
  private var streamVideoClient: StreamVideo? = null
59
72
  private var state: State = State.NOT_INITIALIZED
60
73
  private var overlayView: ComposeView? = null
61
- private var incomingCallView: ComposeView? = null
62
74
  private var barrierView: View? = null
63
75
  private var ringtonePlayer: RingtonePlayer? = null
64
76
  private val mainHandler = Handler(Looper.getMainLooper())
@@ -69,12 +81,17 @@ public class StreamCallPlugin : Plugin() {
69
81
  private var savedActivityPaused = false
70
82
  private var savedCallsToEndOnResume = mutableListOf<Call>()
71
83
  private val callStates: MutableMap<String, LocalCallState> = mutableMapOf()
72
-
84
+
73
85
  // Store current call info
74
86
  private var currentCallId: String = ""
75
87
  private var currentCallType: String = ""
76
88
  private var currentCallState: String = ""
77
89
 
90
+ // Add a field for the fragment
91
+ private var callFragment: StreamCallFragment? = null
92
+ private var streamVideo: StreamVideo? = null
93
+ private var touchInterceptWrapper: TouchInterceptWrapper? = null
94
+
78
95
  private enum class State {
79
96
  NOT_INITIALIZED,
80
97
  INITIALIZING,
@@ -118,7 +135,7 @@ public class StreamCallPlugin : Plugin() {
118
135
  val companionField = callServiceClass.getDeclaredField("Companion")
119
136
  companionField.isAccessible = true
120
137
  val companionInstance = companionField.get(null)
121
-
138
+
122
139
  val removeIncomingCallMethod = companionClass.getDeclaredMethod(
123
140
  "removeIncomingCall",
124
141
  Context::class.java,
@@ -150,64 +167,81 @@ public class StreamCallPlugin : Plugin() {
150
167
  initializeStreamVideo()
151
168
  setupViews()
152
169
  super.load()
170
+ checkPermissions()
171
+ // Register broadcast receiver for ACCEPT_CALL action with high priority
172
+ val filter = IntentFilter("io.getstream.video.android.action.ACCEPT_CALL")
173
+ filter.priority = 999 // Set high priority to ensure it captures the intent
174
+ androidx.core.content.ContextCompat.registerReceiver(activity, acceptCallReceiver, filter, androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED)
175
+ android.util.Log.d("StreamCallPlugin", "Registered broadcast receiver for ACCEPT_CALL action with high priority")
176
+
177
+ // Start the background service to keep the app alive
178
+ val serviceIntent = Intent(activity, StreamCallBackgroundService::class.java)
179
+ activity.startService(serviceIntent)
180
+ android.util.Log.d("StreamCallPlugin", "Started StreamCallBackgroundService to keep app alive")
153
181
 
154
182
  // Handle initial intent if present
155
183
  activity?.intent?.let { handleOnNewIntent(it) }
184
+
185
+ // process the very first intent that started the app (if any)
186
+ pendingIntent?.let {
187
+ android.util.Log.d("StreamCallPlugin","Processing saved initial intent")
188
+ handleOnNewIntent(it)
189
+ pendingIntent = null
190
+ }
156
191
  }
157
192
 
158
193
  @OptIn(DelicateCoroutinesApi::class)
159
194
  override fun handleOnNewIntent(intent: android.content.Intent) {
195
+ android.util.Log.d("StreamCallPlugin", "handleOnNewIntent called: action=${intent.action}, data=${intent.data}, extras=${intent.extras}")
160
196
  super.handleOnNewIntent(intent)
161
-
197
+
162
198
  val action = intent.action
163
199
  val data = intent.data
164
200
  val extras = intent.extras
201
+ android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: Parsed action: $action")
165
202
 
166
203
  if (action === "io.getstream.video.android.action.INCOMING_CALL") {
204
+ android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: Matched INCOMING_CALL action")
167
205
  activity?.runOnUiThread {
168
206
  val cid = intent.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)
207
+ android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - Extracted cid: $cid")
169
208
  if (cid != null) {
209
+ android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - cid is not null, processing.")
170
210
  val call = streamVideoClient?.call(id = cid.id, type = cid.type)
171
- // Start playing ringtone
211
+ android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - Got call object: ${call?.id}")
212
+ // Start ringtone only; UI handled in web layer
172
213
  ringtonePlayer?.startRinging()
173
- // let's set a barrier. This will prevent the user from interacting with the webview while the calling screen is loading
174
- // Launch a coroutine to handle the suspend function
175
- showBarrier()
176
214
 
177
- kotlinx.coroutines.GlobalScope.launch {
178
- call?.get()
179
- activity?.runOnUiThread {
180
- incomingCallView?.setContent {
181
- IncomingCallView(
182
- streamVideo = streamVideoClient,
183
- call = call,
184
- onDeclineCall = { declinedCall ->
185
- declineCall(declinedCall)
186
- },
187
- onAcceptCall = { acceptedCall ->
188
- internalAcceptCall(acceptedCall)
189
- },
190
- onHideIncomingCall = {
191
- hideIncomingCall()
192
- }
193
- )
194
- }
195
- incomingCallView?.isVisible = true
196
- hideBarrier()
215
+ // Notify WebView/JS about incoming call so it can render its own UI
216
+ try {
217
+ val payload = com.getcapacitor.JSObject().apply {
218
+ put("cid", cid.cid)
219
+ put("type", "incoming")
197
220
  }
221
+ notifyListeners("incomingCall", payload, true)
222
+ } catch (e: Exception) {
223
+ android.util.Log.e("StreamCallPlugin", "Error notifying JS about incoming call", e)
198
224
  }
225
+
226
+ bringAppToForeground()
227
+ } else {
228
+ android.util.Log.w("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - cid is null. Cannot process.")
199
229
  }
200
230
  }
201
231
  } else if (action === "io.getstream.video.android.action.ACCEPT_CALL") {
202
- // it's a strategic placed initializeStreamVideo. I want to register the even listeners
203
- // (which are not initialized during the first load in initialization by the application class)
204
- // initializeStreamVideo()
232
+ android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: Matched ACCEPT_CALL action")
205
233
  val cid = intent.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)
234
+ android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: ACCEPT_CALL - Extracted cid: $cid")
206
235
  if (cid != null) {
236
+ android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: ACCEPT_CALL - Accepting call with cid: $cid")
207
237
  val call = streamVideoClient?.call(id = cid.id, type = cid.type)
208
- kotlinx.coroutines.GlobalScope.launch {
209
- call?.get()
210
- call?.let { internalAcceptCall(it) }
238
+ if (call != null) {
239
+ kotlinx.coroutines.GlobalScope.launch {
240
+ internalAcceptCall(call)
241
+ }
242
+ bringAppToForeground()
243
+ } else {
244
+ android.util.Log.e("StreamCallPlugin", "handleOnNewIntent: ACCEPT_CALL - Call object is null for cid: $cid")
211
245
  }
212
246
  }
213
247
  }
@@ -217,18 +251,26 @@ public class StreamCallPlugin : Plugin() {
217
251
  android.util.Log.d("StreamCallPlugin", "New Intent - Extras: $extras")
218
252
  }
219
253
 
254
+ // Public method to handle ACCEPT_CALL intent from MainActivity
255
+ @JvmOverloads
256
+ public fun handleAcceptCallIntent(intent: android.content.Intent) {
257
+ android.util.Log.d("StreamCallPlugin", "handleAcceptCallIntent called: action=${intent.action}")
258
+ handleOnNewIntent(intent)
259
+ }
260
+
220
261
  @OptIn(DelicateCoroutinesApi::class)
221
262
  private fun declineCall(call: Call) {
263
+ android.util.Log.d("StreamCallPlugin", "declineCall called for call: ${call.id}")
222
264
  kotlinx.coroutines.GlobalScope.launch {
223
265
  try {
224
266
  call.reject()
225
-
267
+
226
268
  // Stop ringtone
227
269
  ringtonePlayer?.stopRinging()
228
-
270
+
229
271
  // Notify that call has ended using our helper
230
272
  updateCallStatusAndNotify(call.id, "rejected")
231
-
273
+
232
274
  hideIncomingCall()
233
275
  } catch (e: Exception) {
234
276
  android.util.Log.e("StreamCallPlugin", "Error declining call: ${e.message}")
@@ -238,14 +280,7 @@ public class StreamCallPlugin : Plugin() {
238
280
 
239
281
  private fun hideIncomingCall() {
240
282
  activity?.runOnUiThread {
241
- incomingCallView?.isVisible = false
242
- // Stop ringtone if it's still playing
243
- ringtonePlayer?.stopRinging()
244
- // Check if device is locked using KeyguardManager
245
- val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
246
- if (keyguardManager.isKeyguardLocked) {
247
- activity.moveTaskToBack(true)
248
- }
283
+ // No dedicated incoming-call native view anymore; UI handled by web layer
249
284
  }
250
285
  }
251
286
 
@@ -263,35 +298,46 @@ public class StreamCallPlugin : Plugin() {
263
298
 
264
299
  private fun setupViews() {
265
300
  val context = context
266
- val parent = bridge?.webView?.parent as? ViewGroup ?: return
301
+ val originalParent = bridge?.webView?.parent as? ViewGroup ?: return
302
+
303
+ // Wrap original parent with TouchInterceptWrapper to allow touch passthrough
304
+ val rootParent = originalParent.parent as? ViewGroup
305
+ val indexInRoot = rootParent?.indexOfChild(originalParent) ?: -1
306
+ if (rootParent != null && indexInRoot >= 0) {
307
+ rootParent.removeViewAt(indexInRoot)
308
+ touchInterceptWrapper = TouchInterceptWrapper(originalParent).apply {
309
+ setBackgroundColor(android.graphics.Color.TRANSPARENT)
310
+ }
311
+ rootParent.addView(touchInterceptWrapper, indexInRoot)
312
+ }
267
313
 
268
- // Make WebView transparent
269
- bridge?.webView?.setBackgroundColor(Color.TRANSPARENT)
314
+ val parent: ViewGroup = touchInterceptWrapper ?: originalParent
270
315
 
271
- // Create and add overlay view below WebView
316
+ // Make WebView initially visible and opaque
317
+ bridge?.webView?.setBackgroundColor(Color.WHITE) // Or whatever background color suits your app
318
+
319
+ // Create and add overlay view below WebView for calls
272
320
  overlayView = ComposeView(context).apply {
273
- isVisible = false
321
+ isVisible = false // Start invisible until a call starts
274
322
  layoutParams = FrameLayout.LayoutParams(
275
323
  ViewGroup.LayoutParams.MATCH_PARENT,
276
324
  ViewGroup.LayoutParams.MATCH_PARENT
277
325
  )
278
326
  setContent {
279
- CallOverlayView(
280
- context = context,
281
- streamVideo = streamVideoClient,
282
- call = null
283
- )
327
+ VideoTheme {
328
+ val activeCall = streamVideoClient?.state?.activeCall?.collectAsState()?.value
329
+ if (activeCall != null) {
330
+ CallContent(
331
+ call = activeCall,
332
+ onBackPressed = { /* Handle back press if needed */ }
333
+ )
334
+ }
335
+ }
284
336
  }
285
337
  }
286
338
  parent.addView(overlayView, 0) // Add at index 0 to ensure it's below WebView
287
339
 
288
- val originalContainer: ViewGroup = getBridge().webView
289
-
290
- val wrapper = TouchInterceptWrapper(parent)
291
- (parent.parent as ViewGroup).removeView(originalContainer)
292
- (parent.parent as ViewGroup).addView(wrapper, 0)
293
-
294
- // Create barrier view
340
+ // Create barrier view (above webview for blocking interaction during call setup)
295
341
  barrierView = View(context).apply {
296
342
  isVisible = false
297
343
  layoutParams = FrameLayout.LayoutParams(
@@ -301,19 +347,6 @@ public class StreamCallPlugin : Plugin() {
301
347
  setBackgroundColor(Color.parseColor("#1a242c"))
302
348
  }
303
349
  parent.addView(barrierView, parent.indexOfChild(bridge?.webView) + 1) // Add above WebView
304
-
305
- // Create and add incoming call view
306
- incomingCallView = ComposeView(context).apply {
307
- isVisible = false
308
- layoutParams = FrameLayout.LayoutParams(
309
- ViewGroup.LayoutParams.MATCH_PARENT,
310
- ViewGroup.LayoutParams.MATCH_PARENT
311
- )
312
- setContent {
313
- IncomingCallView(streamVideoClient)
314
- }
315
- }
316
- parent.addView(incomingCallView, parent.indexOfChild(bridge?.webView) + 2) // Add above WebView
317
350
  }
318
351
 
319
352
  @PluginMethod
@@ -363,7 +396,7 @@ public class StreamCallPlugin : Plugin() {
363
396
  try {
364
397
  // Clear stored credentials
365
398
  SecureUserRepository.getInstance(context).removeCurrentUser()
366
-
399
+
367
400
  // Properly cleanup the client
368
401
  kotlinx.coroutines.GlobalScope.launch {
369
402
  streamVideoClient?.let {
@@ -386,7 +419,7 @@ public class StreamCallPlugin : Plugin() {
386
419
 
387
420
  @OptIn(DelicateCoroutinesApi::class)
388
421
  public fun initializeStreamVideo(passedContext: Context? = null, passedApplication: Application? = null) {
389
- android.util.Log.v("StreamCallPlugin", "Attempting to initialize streamVideo")
422
+ android.util.Log.d("StreamCallPlugin", "initializeStreamVideo called")
390
423
  if (state == State.INITIALIZING) {
391
424
  android.util.Log.v("StreamCallPlugin", "Returning, already in the process of initializing")
392
425
  return
@@ -473,9 +506,9 @@ public class StreamCallPlugin : Plugin() {
473
506
  val notificationConfig = NotificationConfig(
474
507
  pushDeviceGenerators = listOf(
475
508
  FirebasePushDeviceGenerator(
476
- providerName = "firebase",
477
- context = contextToUse
478
- )
509
+ providerName = "firebase",
510
+ context = contextToUse
511
+ )
479
512
  ),
480
513
  requestPermissionOnAppLaunch = { true },
481
514
  notificationHandler = notificationHandler,
@@ -506,7 +539,7 @@ public class StreamCallPlugin : Plugin() {
506
539
  }
507
540
 
508
541
  registerEventHandlers()
509
-
542
+
510
543
  android.util.Log.v("StreamCallPlugin", "Initialization finished")
511
544
  initializationTime = System.currentTimeMillis()
512
545
  state = State.INITIALIZED
@@ -571,15 +604,23 @@ public class StreamCallPlugin : Plugin() {
571
604
  // Handle CallCreatedEvent differently - only log it but don't try to access members yet
572
605
  is CallCreatedEvent -> {
573
606
  val callCid = event.callCid
574
- android.util.Log.d("StreamCallPlugin", "Call created: $callCid")
607
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Received for $callCid")
608
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: All members from event: ${event.members.joinToString { it.user.id + " (role: " + it.user.role + ")" }}")
609
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Self user ID from SDK: ${this@StreamCallPlugin.streamVideoClient?.userId}")
575
610
 
576
- // let's get the members
577
- val callParticipants = event.members.filter{ it.user.id != this@StreamCallPlugin.streamVideoClient?.userId }.map { it.user.id }
578
- android.util.Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} participants")
611
+ val callParticipants = event.members.filter {
612
+ val selfId = this@StreamCallPlugin.streamVideoClient?.userId
613
+ val memberId = it.user.id
614
+ val isSelf = memberId == selfId
615
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Filtering member $memberId. Self ID: $selfId. Is self: $isSelf")
616
+ !isSelf
617
+ }.map { it.user.id }
618
+
619
+ android.util.Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} remote participants: ${callParticipants.joinToString()}.")
579
620
 
580
621
  // Start tracking this call now that we have the member list
581
622
  startCallTimeoutMonitor(callCid, callParticipants)
582
-
623
+
583
624
  // Use direction from event if available
584
625
  val callType = callCid.split(":").firstOrNull() ?: "default"
585
626
  updateCallStatusAndNotify(callCid, "created")
@@ -589,61 +630,61 @@ public class StreamCallPlugin : Plugin() {
589
630
  val callCid = event.callCid
590
631
  updateCallStatusAndNotify(callCid, "session_started")
591
632
  }
592
-
633
+
593
634
  is CallRejectedEvent -> {
594
635
  val userId = event.user.id
595
636
  val callCid = event.callCid
596
-
637
+
597
638
  // Update call state
598
639
  callStates[callCid]?.let { callState ->
599
640
  callState.participantResponses[userId] = "rejected"
600
641
  }
601
-
642
+
602
643
  updateCallStatusAndNotify(callCid, "rejected", userId)
603
-
644
+
604
645
  // Check if all participants have responded
605
646
  checkAllParticipantsResponded(callCid)
606
647
  }
607
-
648
+
608
649
  is CallMissedEvent -> {
609
650
  val userId = event.user.id
610
651
  val callCid = event.callCid
611
-
652
+
612
653
  // Update call state
613
654
  callStates[callCid]?.let { callState ->
614
655
  callState.participantResponses[userId] = "missed"
615
656
  }
616
-
657
+
617
658
  val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
618
659
  if (keyguardManager.isKeyguardLocked) {
619
660
  android.util.Log.d("StreamCallPlugin", "Stop ringing and move to background")
620
661
  this.ringtonePlayer?.stopRinging()
621
662
  moveAllActivitiesToBackgroundOrKill(context)
622
663
  }
623
-
664
+
624
665
  updateCallStatusAndNotify(callCid, "missed", userId)
625
-
666
+
626
667
  // Check if all participants have responded
627
668
  checkAllParticipantsResponded(callCid)
628
669
  }
629
-
670
+
630
671
  is CallAcceptedEvent -> {
631
672
  val userId = event.user.id
632
673
  val callCid = event.callCid
633
-
674
+
634
675
  // Update call state
635
676
  callStates[callCid]?.let { callState ->
636
677
  callState.participantResponses[userId] = "accepted"
637
-
678
+
638
679
  // Since someone accepted, cancel the timeout timer
639
680
  android.util.Log.d("StreamCallPlugin", "Call accepted by $userId, canceling timeout timer for $callCid")
640
681
  callState.timer?.removeCallbacksAndMessages(null)
641
682
  callState.timer = null
642
683
  }
643
-
684
+
644
685
  updateCallStatusAndNotify(callCid, "accepted", userId)
645
686
  }
646
-
687
+
647
688
  is CallEndedEvent -> {
648
689
  runOnMainThread {
649
690
  android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallEndedEvent for call ${event.callCid}")
@@ -653,7 +694,7 @@ public class StreamCallPlugin : Plugin() {
653
694
  }
654
695
  updateCallStatusAndNotify(event.callCid, "left")
655
696
  }
656
-
697
+
657
698
  is CallSessionEndedEvent -> {
658
699
  runOnMainThread {
659
700
  android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallSessionEndedEvent for call ${event.callCid}. Test session: ${event.call.session?.endedAt}")
@@ -663,7 +704,7 @@ public class StreamCallPlugin : Plugin() {
663
704
  }
664
705
  updateCallStatusAndNotify(event.callCid, "left")
665
706
  }
666
-
707
+
667
708
  else -> {
668
709
  updateCallStatusAndNotify(
669
710
  streamVideoClient?.state?.activeCall?.value?.cid ?: "",
@@ -749,6 +790,7 @@ public class StreamCallPlugin : Plugin() {
749
790
 
750
791
  @PluginMethod
751
792
  public fun acceptCall(call: PluginCall) {
793
+ android.util.Log.d("StreamCallPlugin", "acceptCall called")
752
794
  try {
753
795
  val streamVideoCall = streamVideoClient?.state?.ringingCall?.value
754
796
  if (streamVideoCall == null) {
@@ -766,6 +808,7 @@ public class StreamCallPlugin : Plugin() {
766
808
 
767
809
  @PluginMethod
768
810
  public fun rejectCall(call: PluginCall) {
811
+ android.util.Log.d("StreamCallPlugin", "rejectCall called")
769
812
  try {
770
813
  val streamVideoCall = streamVideoClient?.state?.ringingCall?.value
771
814
  if (streamVideoCall == null) {
@@ -782,38 +825,120 @@ public class StreamCallPlugin : Plugin() {
782
825
  }
783
826
 
784
827
  @OptIn(DelicateCoroutinesApi::class)
785
- private fun internalAcceptCall(call: Call) {
828
+ internal fun internalAcceptCall(call: Call) {
829
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Entered for call: ${call.id}")
786
830
  kotlinx.coroutines.GlobalScope.launch {
787
831
  try {
832
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Coroutine started for call ${call.id}")
788
833
  // Stop ringtone
789
834
  ringtonePlayer?.stopRinging()
790
-
835
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Ringtone player stopped for call ${call.id}")
836
+
791
837
  // Hide incoming call view first
792
838
  runOnMainThread {
793
- android.util.Log.d("StreamCallPlugin", "Hiding incoming call view for call ${call.id}")
794
- incomingCallView?.isVisible = false
839
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Hiding incoming call view for call ${call.id}")
840
+ // No dedicated incoming-call native view anymore; UI handled by web layer
841
+ }
842
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Incoming call view hidden for call ${call.id}")
843
+
844
+ // Check and request permissions before joining the call
845
+ val permissionsGranted = checkPermissions()
846
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: checkPermissions result for call ${call.id}: $permissionsGranted")
847
+ if (!permissionsGranted) {
848
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Permissions not granted for call ${call.id}. Requesting permissions.")
849
+ requestPermissions()
850
+ // Do not proceed with joining until permissions are granted
851
+ runOnMainThread {
852
+ android.widget.Toast.makeText(
853
+ context,
854
+ "Permissions required for call. Please grant them.",
855
+ android.widget.Toast.LENGTH_LONG
856
+ ).show()
857
+ }
858
+ android.util.Log.w("StreamCallPlugin", "internalAcceptCall: Permissions not granted for call ${call.id}. Aborting accept process.")
859
+ return@launch
795
860
  }
796
861
 
862
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Permissions are granted for call ${call.id}. Proceeding to accept.")
797
863
  // Join the call without affecting others
798
864
  call.accept()
865
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: call.accept() completed for call ${call.id}")
866
+ call.join()
867
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: call.join() completed for call ${call.id}")
868
+ streamVideoClient?.state?.setActiveCall(call)
869
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: setActiveCall completed for call ${call.id}")
799
870
 
800
871
  // Notify that call has started using helper
801
872
  updateCallStatusAndNotify(call.id, "joined")
873
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: updateCallStatusAndNotify(joined) called for ${call.id}")
802
874
 
803
- // Show overlay view with the active call
875
+ // Show overlay view with the active call and make webview transparent
804
876
  runOnMainThread {
805
- android.util.Log.d("StreamCallPlugin", "Setting overlay visible after accepting call ${call.id}")
877
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Updating UI for active call ${call.id} - setting overlay visible.")
878
+ bridge?.webView?.setBackgroundColor(Color.TRANSPARENT) // Make webview transparent
879
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: WebView background set to transparent for call ${call.id}")
880
+ bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
881
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: WebView brought to front for call ${call.id}")
882
+ // Reusing the initialization logic from call method
883
+ call.microphone?.setEnabled(true)
884
+ call.camera?.setEnabled(true)
885
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Microphone and camera enabled for call ${call.id}")
806
886
  overlayView?.setContent {
807
- CallOverlayView(
808
- context = context,
809
- streamVideo = streamVideoClient,
810
- call = call
811
- )
887
+ VideoTheme {
888
+ if (call != null) {
889
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Setting CallContent with active call ${call.id}")
890
+ CallContent(
891
+ call = call,
892
+ onBackPressed = { /* ... */ },
893
+ controlsContent = { /* ... */ },
894
+ appBarContent = { /* ... */ }
895
+ )
896
+ } else {
897
+ android.util.Log.w("StreamCallPlugin", "internalAcceptCall: Active call is null, cannot set CallContent for call ${call.id}")
898
+ }
899
+ }
812
900
  }
901
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Content set for overlayView for call ${call.id}")
813
902
  overlayView?.isVisible = true
903
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: OverlayView set to visible for call ${call.id}, isVisible: ${overlayView?.isVisible}")
904
+
905
+ // Ensure overlay is behind WebView by adjusting its position in the parent
906
+ val parent = overlayView?.parent as? ViewGroup
907
+ parent?.removeView(overlayView)
908
+ parent?.addView(overlayView, 0) // Add at index 0 to ensure it's behind other views
909
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: OverlayView re-added to parent at index 0 for call ${call.id}")
910
+ // Add a small delay to ensure UI refresh
911
+ mainHandler.postDelayed({
912
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Delayed UI check, overlay visible: ${overlayView?.isVisible} for call ${call.id}")
913
+ if (overlayView?.isVisible == true) {
914
+ overlayView?.invalidate()
915
+ overlayView?.requestLayout()
916
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: UI invalidated and layout requested for call ${call.id}")
917
+ // Force refresh with active call from client
918
+ val activeCall = streamVideoClient?.state?.activeCall?.value
919
+ if (activeCall != null) {
920
+ overlayView?.setContent {
921
+ VideoTheme {
922
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Force refreshing CallContent with active call ${activeCall.id}")
923
+ CallContent(
924
+ call = activeCall,
925
+ onBackPressed = { /* ... */ },
926
+ controlsContent = { /* ... */ },
927
+ appBarContent = { /* ... */ }
928
+ )
929
+ }
930
+ }
931
+ android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Content force refreshed for call ${activeCall.id}")
932
+ } else {
933
+ android.util.Log.w("StreamCallPlugin", "internalAcceptCall: Active call is null during force refresh for call ${call.id}")
934
+ }
935
+ } else {
936
+ android.util.Log.w("StreamCallPlugin", "internalAcceptCall: overlayView not visible after delay for call ${call.id}")
937
+ }
938
+ }, 1000) // Increased delay to ensure all events are processed
814
939
  }
815
940
  } catch (e: Exception) {
816
- android.util.Log.e("StreamCallPlugin", "Error accepting call ${call.id}: ${e.message}")
941
+ android.util.Log.e("StreamCallPlugin", "internalAcceptCall: Error accepting call ${call.id}: ${e.message}", e)
817
942
  runOnMainThread {
818
943
  android.widget.Toast.makeText(
819
944
  context,
@@ -825,6 +950,74 @@ public class StreamCallPlugin : Plugin() {
825
950
  }
826
951
  }
827
952
 
953
+ // Function to check required permissions
954
+ private fun checkPermissions(): Boolean {
955
+ android.util.Log.d("StreamCallPlugin", "checkPermissions: Entered")
956
+ val audioPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
957
+ android.util.Log.d("StreamCallPlugin", "checkPermissions: RECORD_AUDIO permission status: $audioPermission (Granted=${PackageManager.PERMISSION_GRANTED})")
958
+ val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
959
+ android.util.Log.d("StreamCallPlugin", "checkPermissions: CAMERA permission status: $cameraPermission (Granted=${PackageManager.PERMISSION_GRANTED})")
960
+ val allGranted = audioPermission == PackageManager.PERMISSION_GRANTED && cameraPermission == PackageManager.PERMISSION_GRANTED
961
+ android.util.Log.d("StreamCallPlugin", "checkPermissions: All permissions granted: $allGranted")
962
+ return allGranted
963
+ }
964
+
965
+ // Function to request required permissions
966
+ private fun requestPermissions() {
967
+ android.util.Log.d("StreamCallPlugin", "requestPermissions: Requesting RECORD_AUDIO and CAMERA permissions.")
968
+ ActivityCompat.requestPermissions(
969
+ activity,
970
+ arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA),
971
+ 1001 // Request code for permission result handling
972
+ )
973
+ android.util.Log.d("StreamCallPlugin", "requestPermissions: ActivityCompat.requestPermissions called.")
974
+ }
975
+
976
+ // Override to handle permission results
977
+ override fun handleRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
978
+ super.handleRequestPermissionsResult(requestCode, permissions, grantResults)
979
+ android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Entered. RequestCode: $requestCode")
980
+ if (requestCode == 1001) {
981
+ android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Matched requestCode 1001.")
982
+ logPermissionResults(permissions, grantResults)
983
+ if (grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
984
+ android.util.Log.i("StreamCallPlugin", "handleRequestPermissionsResult: All permissions GRANTED.")
985
+ // Permissions granted, can attempt to join the call again if needed
986
+ val ringingCall = streamVideoClient?.state?.ringingCall?.value
987
+ android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Ringing call object: ${ringingCall?.id}")
988
+ if (ringingCall != null) {
989
+ android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Ringing call found (${ringingCall.id}). Re-attempting internalAcceptCall.")
990
+ kotlinx.coroutines.GlobalScope.launch {
991
+ internalAcceptCall(ringingCall)
992
+ }
993
+ } else {
994
+ android.util.Log.w("StreamCallPlugin", "handleRequestPermissionsResult: Permissions granted, but no ringing call found to accept.")
995
+ }
996
+ } else {
997
+ android.util.Log.e("StreamCallPlugin", "handleRequestPermissionsResult: One or more permissions DENIED.")
998
+ runOnMainThread {
999
+ android.widget.Toast.makeText(
1000
+ context,
1001
+ "Permissions not granted. Cannot join call.",
1002
+ android.widget.Toast.LENGTH_LONG
1003
+ ).show()
1004
+ }
1005
+ }
1006
+ } else {
1007
+ android.util.Log.w("StreamCallPlugin", "handleRequestPermissionsResult: Received unknown requestCode: $requestCode")
1008
+ }
1009
+ }
1010
+
1011
+ private fun logPermissionResults(permissions: Array<out String>, grantResults: IntArray) {
1012
+ android.util.Log.d("StreamCallPlugin", "logPermissionResults: Logging permission results:")
1013
+ for (i in permissions.indices) {
1014
+ val permission = permissions[i]
1015
+ val grantResult = if (grantResults.size > i) grantResults[i] else -999 // -999 for safety if arrays mismatch
1016
+ val resultString = if (grantResult == PackageManager.PERMISSION_GRANTED) "GRANTED" else "DENIED ($grantResult)"
1017
+ android.util.Log.d("StreamCallPlugin", " Permission: $permission, Result: $resultString")
1018
+ }
1019
+ }
1020
+
828
1021
  @OptIn(DelicateCoroutinesApi::class)
829
1022
  @PluginMethod
830
1023
  fun setMicrophoneEnabled(call: PluginCall) {
@@ -921,14 +1114,14 @@ public class StreamCallPlugin : Plugin() {
921
1114
  val callId = call.id
922
1115
  android.util.Log.d("StreamCallPlugin", "Attempting to end call $callId")
923
1116
  call.leave()
924
-
1117
+
925
1118
  // Capture context from the overlayView
926
1119
  val currentContext = overlayView?.context ?: this.savedContext
927
1120
  if (currentContext == null) {
928
1121
  android.util.Log.w("StreamCallPlugin", "Cannot end call $callId because context is null")
929
1122
  return
930
1123
  }
931
-
1124
+
932
1125
  runOnMainThread {
933
1126
  android.util.Log.d("StreamCallPlugin", "Setting overlay invisible after ending call $callId")
934
1127
 
@@ -959,20 +1152,24 @@ public class StreamCallPlugin : Plugin() {
959
1152
  }
960
1153
 
961
1154
  overlayView?.setContent {
962
- CallOverlayView(
963
- context = currentContext,
964
- streamVideo = streamVideoClient,
965
- call = null
966
- )
1155
+ VideoTheme {
1156
+ CallContent(
1157
+ call = call,
1158
+ onBackPressed = { /* Handle back press if needed */ },
1159
+ controlsContent = { /* Empty to disable native controls */ },
1160
+ appBarContent = { /* Empty to disable app bar with stop call button */ }
1161
+ )
1162
+ }
967
1163
  }
968
1164
  overlayView?.isVisible = false
1165
+ bridge?.webView?.setBackgroundColor(Color.WHITE) // Restore webview opacity
969
1166
  this@StreamCallPlugin.ringtonePlayer?.stopRinging()
970
1167
 
971
1168
  // Also hide incoming call view if visible
972
1169
  android.util.Log.d("StreamCallPlugin", "Hiding incoming call view for call $callId")
973
- incomingCallView?.isVisible = false
1170
+ // No dedicated incoming-call native view anymore; UI handled by web layer
974
1171
  }
975
-
1172
+
976
1173
  // Notify that call has ended using helper
977
1174
  updateCallStatusAndNotify(callId, "left")
978
1175
  }
@@ -1068,15 +1265,22 @@ public class StreamCallPlugin : Plugin() {
1068
1265
  android.util.Log.d("StreamCallPlugin", "- Users: $userIds")
1069
1266
  android.util.Log.d("StreamCallPlugin", "- Should Ring: $shouldRing")
1070
1267
 
1268
+ // Check permissions before creating the call
1269
+ if (!checkPermissions()) {
1270
+ requestPermissions()
1271
+ call.reject("Permissions required for call. Please grant them.")
1272
+ return
1273
+ }
1274
+
1071
1275
  // Create and join call in a coroutine
1072
1276
  kotlinx.coroutines.GlobalScope.launch {
1073
1277
  try {
1074
1278
  // Create the call object
1075
1279
  val streamCall = streamVideoClient?.call(type = callType, id = callId)
1076
-
1280
+
1077
1281
  // Note: We no longer start tracking here - we'll wait for CallSessionStartedEvent
1078
1282
  // instead, which contains the actual participant list
1079
-
1283
+
1080
1284
  android.util.Log.d("StreamCallPlugin", "Creating call with members...")
1081
1285
  // Create the call with all members
1082
1286
  val createResult = streamCall?.create(
@@ -1085,9 +1289,9 @@ public class StreamCallPlugin : Plugin() {
1085
1289
  ring = shouldRing,
1086
1290
  team = team,
1087
1291
  )
1088
-
1292
+
1089
1293
  if (createResult?.isFailure == true) {
1090
- throw (createResult.errorOrNull() ?: RuntimeException("Unknown error creating call")) as Throwable
1294
+ throw (createResult.errorOrNull() ?: RuntimeException("Unknown error creating call")) as Throwable
1091
1295
  }
1092
1296
 
1093
1297
  android.util.Log.d("StreamCallPlugin", "Setting overlay visible for outgoing call $callId")
@@ -1096,14 +1300,25 @@ public class StreamCallPlugin : Plugin() {
1096
1300
  streamCall?.microphone?.setEnabled(true)
1097
1301
  streamCall?.camera?.setEnabled(true)
1098
1302
 
1303
+ bridge?.webView?.setBackgroundColor(Color.TRANSPARENT) // Make webview transparent
1304
+ bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
1099
1305
  overlayView?.setContent {
1100
- CallOverlayView(
1101
- context = context,
1102
- streamVideo = streamVideoClient,
1103
- call = streamCall
1104
- )
1306
+ VideoTheme {
1307
+ if (streamCall != null) {
1308
+ CallContent(
1309
+ call = streamCall,
1310
+ onBackPressed = { /* Handle back press if needed */ },
1311
+ controlsContent = { /* Empty to disable native controls */ },
1312
+ appBarContent = { /* Empty to disable app bar with stop call button */ }
1313
+ )
1314
+ }
1315
+ }
1105
1316
  }
1106
1317
  overlayView?.isVisible = true
1318
+ // Ensure overlay is behind WebView by adjusting its position in the parent
1319
+ val parent = overlayView?.parent as? ViewGroup
1320
+ parent?.removeView(overlayView)
1321
+ parent?.addView(overlayView, 0) // Add at index 0 to ensure it's behind other views
1107
1322
  }
1108
1323
 
1109
1324
  // Resolve the call with success
@@ -1122,7 +1337,7 @@ public class StreamCallPlugin : Plugin() {
1122
1337
 
1123
1338
  private fun startCallTimeoutMonitor(callCid: String, memberIds: List<String>) {
1124
1339
  val callState = LocalCallState(members = memberIds)
1125
-
1340
+
1126
1341
  val handler = Handler(Looper.getMainLooper())
1127
1342
  val timeoutRunnable = object : Runnable {
1128
1343
  override fun run() {
@@ -1130,55 +1345,55 @@ public class StreamCallPlugin : Plugin() {
1130
1345
  handler.postDelayed(this, 1000)
1131
1346
  }
1132
1347
  }
1133
-
1348
+
1134
1349
  handler.postDelayed(timeoutRunnable, 1000)
1135
1350
  callState.timer = handler
1136
-
1351
+
1137
1352
  callStates[callCid] = callState
1138
-
1353
+
1139
1354
  android.util.Log.d("StreamCallPlugin", "Started timeout monitor for call $callCid with ${memberIds.size} members")
1140
1355
  }
1141
1356
 
1142
1357
  private fun checkCallTimeout(callCid: String) {
1143
1358
  val callState = callStates[callCid] ?: return
1144
-
1359
+
1145
1360
  val now = System.currentTimeMillis()
1146
1361
  val elapsedSeconds = (now - callState.createdAt) / 1000
1147
-
1362
+
1148
1363
  if (elapsedSeconds >= 30) {
1149
1364
  android.util.Log.d("StreamCallPlugin", "Call $callCid has timed out after $elapsedSeconds seconds")
1150
-
1365
+
1151
1366
  val hasAccepted = callState.participantResponses.values.any { it == "accepted" }
1152
-
1367
+
1153
1368
  if (!hasAccepted) {
1154
1369
  android.util.Log.d("StreamCallPlugin", "No one accepted call $callCid, marking all non-responders as missed")
1155
-
1370
+
1156
1371
  // First, remove the timer to prevent further callbacks
1157
1372
  callState.timer?.removeCallbacksAndMessages(null)
1158
1373
  callState.timer = null
1159
-
1374
+
1160
1375
  callState.members.forEach { memberId ->
1161
1376
  if (memberId !in callState.participantResponses) {
1162
1377
  callState.participantResponses[memberId] = "missed"
1163
-
1378
+
1164
1379
  updateCallStatusAndNotify(callCid, "missed", memberId)
1165
1380
  }
1166
1381
  }
1167
-
1382
+
1168
1383
  val callIdParts = callCid.split(":")
1169
1384
  if (callIdParts.size >= 2) {
1170
1385
  val callType = callIdParts[0]
1171
1386
  val callId = callIdParts[1]
1172
-
1387
+
1173
1388
  streamVideoClient?.call(type = callType, id = callId)?.let { call ->
1174
1389
  kotlinx.coroutines.GlobalScope.launch {
1175
1390
  try {
1176
1391
  // Use endCallRaw instead of manual cleanup
1177
1392
  endCallRaw(call)
1178
-
1393
+
1179
1394
  // Clean up state - we don't need to do this in endCallRaw because we already did it here
1180
1395
  callStates.remove(callCid)
1181
-
1396
+
1182
1397
  // Notify that call has ended using helper
1183
1398
  updateCallStatusAndNotify(callCid, "ended", null, "timeout")
1184
1399
  } catch (e: Exception) {
@@ -1194,62 +1409,62 @@ public class StreamCallPlugin : Plugin() {
1194
1409
  private fun cleanupCall(callCid: String) {
1195
1410
  // Get the call state
1196
1411
  val callState = callStates[callCid]
1197
-
1412
+
1198
1413
  if (callState != null) {
1199
1414
  // Ensure timer is properly canceled
1200
1415
  android.util.Log.d("StreamCallPlugin", "Stopping timer for call: $callCid")
1201
1416
  callState.timer?.removeCallbacksAndMessages(null)
1202
1417
  callState.timer = null
1203
1418
  }
1204
-
1419
+
1205
1420
  // Remove from callStates
1206
1421
  callStates.remove(callCid)
1207
-
1422
+
1208
1423
  // Hide UI elements directly without setting content
1209
1424
  runOnMainThread {
1210
1425
  android.util.Log.d("StreamCallPlugin", "Hiding UI elements for call $callCid (one-time cleanup)")
1211
1426
  overlayView?.isVisible = false
1212
1427
  ringtonePlayer?.stopRinging()
1213
- incomingCallView?.isVisible = false
1428
+ // No dedicated incoming-call native view anymore; UI handled by web layer
1214
1429
  }
1215
-
1430
+
1216
1431
  android.util.Log.d("StreamCallPlugin", "Cleaned up resources for ended call: $callCid")
1217
1432
  }
1218
1433
 
1219
1434
  private fun checkAllParticipantsResponded(callCid: String) {
1220
1435
  val callState = callStates[callCid] ?: return
1221
-
1436
+
1222
1437
  val totalParticipants = callState.members.size
1223
1438
  val responseCount = callState.participantResponses.size
1224
-
1439
+
1225
1440
  android.util.Log.d("StreamCallPlugin", "Checking responses for call $callCid: $responseCount / $totalParticipants")
1226
-
1441
+
1227
1442
  val allResponded = responseCount >= totalParticipants
1228
- val allRejectedOrMissed = allResponded &&
1229
- callState.participantResponses.values.all { it == "rejected" || it == "missed" }
1230
-
1443
+ val allRejectedOrMissed = allResponded &&
1444
+ callState.participantResponses.values.all { it == "rejected" || it == "missed" }
1445
+
1231
1446
  if (allResponded && allRejectedOrMissed) {
1232
1447
  android.util.Log.d("StreamCallPlugin", "All participants have rejected or missed the call $callCid")
1233
-
1448
+
1234
1449
  // Cancel the timer immediately to prevent further callbacks
1235
1450
  callState.timer?.removeCallbacksAndMessages(null)
1236
1451
  callState.timer = null
1237
-
1452
+
1238
1453
  // End the call using endCallRaw
1239
1454
  val callIdParts = callCid.split(":")
1240
1455
  if (callIdParts.size >= 2) {
1241
1456
  val callType = callIdParts[0]
1242
1457
  val callId = callIdParts[1]
1243
-
1458
+
1244
1459
  streamVideoClient?.call(type = callType, id = callId)?.let { call ->
1245
1460
  kotlinx.coroutines.GlobalScope.launch {
1246
1461
  try {
1247
1462
  // Use endCallRaw instead of manual cleanup
1248
1463
  endCallRaw(call)
1249
-
1464
+
1250
1465
  // Clean up state - we don't need to do this in endCallRaw because we already did it here
1251
1466
  callStates.remove(callCid)
1252
-
1467
+
1253
1468
  // Notify that call has ended using helper
1254
1469
  updateCallStatusAndNotify(callCid, "ended", null, "all_rejected_or_missed")
1255
1470
  } catch (e: Exception) {
@@ -1291,23 +1506,58 @@ public class StreamCallPlugin : Plugin() {
1291
1506
  val result = JSObject()
1292
1507
  result.put("callId", currentCallId)
1293
1508
  result.put("state", currentCallState)
1294
-
1509
+
1295
1510
  // No additional fields to ensure compatibility with CallEvent interface
1296
-
1511
+
1297
1512
  call.resolve(result)
1298
1513
  }
1299
1514
 
1515
+ @PluginMethod
1516
+ fun setSpeaker(call: PluginCall) {
1517
+ val name = call.getString("name") ?: "speaker"
1518
+ val activeCall = streamVideoClient?.state?.activeCall?.value
1519
+ if (activeCall != null) {
1520
+ if (name == "speaker")
1521
+ activeCall.speaker.setSpeakerPhone(enable = true)
1522
+ else
1523
+ activeCall.speaker.setSpeakerPhone(enable = false)
1524
+ call.resolve(JSObject().apply {
1525
+ put("success", true)
1526
+ })
1527
+ } else {
1528
+ call.reject("No active call")
1529
+ }
1530
+ }
1531
+
1532
+ @PluginMethod
1533
+ fun switchCamera(call: PluginCall) {
1534
+ val camera = call.getString("camera") ?: "front"
1535
+ val activeCall = streamVideoClient?.state?.activeCall?.value
1536
+ if (activeCall != null) {
1537
+ if (camera == "front")
1538
+ activeCall.camera.setDirection(CameraDirection.Front)
1539
+ else
1540
+ activeCall.camera.setDirection(CameraDirection.Back)
1541
+ call.resolve(JSObject().apply {
1542
+ put("success", true)
1543
+ })
1544
+ } else {
1545
+ call.reject("No active call")
1546
+ }
1547
+ }
1548
+
1300
1549
  // Helper method to update call status and notify listeners
1301
1550
  private fun updateCallStatusAndNotify(callId: String, state: String, userId: String? = null, reason: String? = null) {
1551
+ android.util.Log.d("StreamCallPlugin", "updateCallStatusAndNotify called: callId=$callId, state=$state, userId=$userId, reason=$reason")
1302
1552
  // Update stored call info
1303
1553
  currentCallId = callId
1304
1554
  currentCallState = state
1305
-
1555
+
1306
1556
  // Get call type from call ID if available
1307
1557
  if (callId.contains(":")) {
1308
1558
  currentCallType = callId.split(":").firstOrNull() ?: ""
1309
1559
  }
1310
-
1560
+
1311
1561
  // Create data object with only the fields in the CallEvent interface
1312
1562
  val data = JSObject().apply {
1313
1563
  put("callId", callId)
@@ -1319,15 +1569,102 @@ public class StreamCallPlugin : Plugin() {
1319
1569
  put("reason", it)
1320
1570
  }
1321
1571
  }
1322
-
1572
+
1323
1573
  // Notify listeners
1324
1574
  notifyListeners("callEvent", data)
1325
1575
  }
1326
1576
 
1577
+ @PluginMethod
1578
+ fun joinCall(call: PluginCall) {
1579
+ val fragment = callFragment
1580
+ if (fragment != null && fragment.getCall() != null) {
1581
+ if (!checkPermissions()) {
1582
+ requestPermissions()
1583
+ call.reject("Permissions required for call. Please grant them.")
1584
+ return
1585
+ }
1586
+ CoroutineScope(Dispatchers.Main).launch {
1587
+ fragment.getCall()?.join()
1588
+ call.resolve()
1589
+ }
1590
+ } else {
1591
+ call.reject("No active call or fragment not initialized")
1592
+ }
1593
+ }
1594
+
1595
+ @PluginMethod
1596
+ fun leaveCall(call: PluginCall) {
1597
+ val fragment = callFragment
1598
+ if (fragment != null && fragment.getCall() != null) {
1599
+ CoroutineScope(Dispatchers.Main).launch {
1600
+ fragment.getCall()?.leave()
1601
+ call.resolve()
1602
+ }
1603
+ } else {
1604
+ call.reject("No active call or fragment not initialized")
1605
+ }
1606
+ }
1607
+
1327
1608
  data class LocalCallState(
1328
1609
  val members: List<String>,
1329
1610
  val participantResponses: MutableMap<String, String> = mutableMapOf(),
1330
1611
  val createdAt: Long = System.currentTimeMillis(),
1331
1612
  var timer: Handler? = null
1332
1613
  )
1614
+
1615
+ private val acceptCallReceiver = object : BroadcastReceiver() {
1616
+ override fun onReceive(context: Context?, intent: Intent?) {
1617
+ android.util.Log.d("StreamCallPlugin", "BroadcastReceiver: Received broadcast with action: ${intent?.action}")
1618
+ if (intent?.action == "io.getstream.video.android.action.ACCEPT_CALL") {
1619
+ val cid = intent.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)
1620
+ android.util.Log.d("StreamCallPlugin", "BroadcastReceiver: ACCEPT_CALL broadcast received with cid: $cid")
1621
+ if (cid != null) {
1622
+ android.util.Log.d("StreamCallPlugin", "BroadcastReceiver: Accepting call with cid: $cid")
1623
+ val call = streamVideoClient?.call(id = cid.id, type = cid.type)
1624
+ if (call != null) {
1625
+ kotlinx.coroutines.GlobalScope.launch {
1626
+ internalAcceptCall(call)
1627
+ }
1628
+ bringAppToForeground()
1629
+ } else {
1630
+ android.util.Log.e("StreamCallPlugin", "BroadcastReceiver: Call object is null for cid: $cid")
1631
+ }
1632
+ }
1633
+ }
1634
+ }
1635
+ }
1636
+
1637
+ private fun bringAppToForeground() {
1638
+ try {
1639
+ val ctx = savedContext ?: context
1640
+ val launchIntent = ctx.packageManager.getLaunchIntentForPackage(ctx.packageName)
1641
+ launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP)
1642
+ if (launchIntent != null) {
1643
+ ctx.startActivity(launchIntent)
1644
+ android.util.Log.d("StreamCallPlugin", "bringAppToForeground: Launch intent executed to foreground app")
1645
+ } else {
1646
+ android.util.Log.w("StreamCallPlugin", "bringAppToForeground: launchIntent is null")
1647
+ }
1648
+ } catch (e: Exception) {
1649
+ android.util.Log.e("StreamCallPlugin", "bringAppToForeground error", e)
1650
+ }
1651
+ }
1652
+
1653
+ companion object {
1654
+ private var pendingIntent: Intent? = null
1655
+ @JvmStatic fun saveInitialIntent(it: Intent) {
1656
+ pendingIntent = it
1657
+ }
1658
+ @JvmStatic fun preLoadInit(ctx: Context, app: Application) {
1659
+ holder ?: run {
1660
+ val p = StreamCallPlugin()
1661
+ p.savedContext = ctx
1662
+ p.initializeStreamVideo(ctx, app)
1663
+ holder = p
1664
+ // record the intent that started the process
1665
+ if (ctx is Activity) saveInitialIntent((ctx as Activity).intent)
1666
+ }
1667
+ }
1668
+ private var holder: StreamCallPlugin? = null
1669
+ }
1333
1670
  }