@capgo/capacitor-stream-call 0.0.82 → 0.0.84

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.
@@ -20,15 +20,14 @@ import android.os.Bundle
20
20
  import android.os.Handler
21
21
  import android.os.Looper
22
22
  import android.provider.Settings
23
+ import android.util.Log
23
24
  import android.view.View
24
25
  import android.view.ViewGroup
25
26
  import android.view.WindowManager
26
27
  import android.widget.FrameLayout
27
28
  import androidx.compose.foundation.layout.fillMaxSize
28
29
  import androidx.compose.runtime.collectAsState
29
- import androidx.compose.runtime.derivedStateOf
30
30
  import androidx.compose.runtime.getValue
31
- import androidx.compose.runtime.remember
32
31
  import androidx.compose.ui.Modifier
33
32
  import androidx.compose.ui.draw.clip
34
33
  import androidx.compose.ui.platform.ComposeView
@@ -87,11 +86,13 @@ import kotlinx.coroutines.launch
87
86
  import kotlinx.coroutines.tasks.await
88
87
  import androidx.core.net.toUri
89
88
  import org.json.JSONObject
89
+ import androidx.core.graphics.toColorInt
90
+ import androidx.core.content.edit
90
91
 
91
92
  // I am not a religious pearson, but at this point, I am not sure even god himself would understand this code
92
93
  // It's a spaghetti-like, tangled, unreadable mess and frankly, I am deeply sorry for the code crimes commited in the Android impl
93
94
  @CapacitorPlugin(name = "StreamCall")
94
- public class StreamCallPlugin : Plugin() {
95
+ class StreamCallPlugin : Plugin() {
95
96
  private var streamVideoClient: StreamVideo? = null
96
97
  private var state: State = State.NOT_INITIALIZED
97
98
  private var overlayView: ComposeView? = null
@@ -108,6 +109,8 @@ public class StreamCallPlugin : Plugin() {
108
109
  private var activeCallStateJob: Job? = null
109
110
  private var cameraStatusJob: Job? = null
110
111
  private var microphoneStatusJob: Job? = null
112
+ private var lastEventSent: String? = null
113
+ private var callIsAudioOnly: Boolean = false
111
114
 
112
115
  // Store current call info
113
116
  private var currentCallId: String = ""
@@ -116,7 +119,6 @@ public class StreamCallPlugin : Plugin() {
116
119
 
117
120
  // Add a field for the fragment
118
121
  private var callFragment: StreamCallFragment? = null
119
- private var streamVideo: StreamVideo? = null
120
122
  private var touchInterceptWrapper: TouchInterceptWrapper? = null
121
123
 
122
124
  // Track permission request timing and attempts
@@ -131,6 +133,7 @@ public class StreamCallPlugin : Plugin() {
131
133
  private var pendingCallTeam: String? = null
132
134
  private var pendingCustomObject: JSObject? = null
133
135
  private var pendingAcceptCall: Call? = null // Store the actual call object for acceptance
136
+ private var pendingSetCameraCall: PluginCall? = null
134
137
 
135
138
  private enum class State {
136
139
  NOT_INITIALIZED,
@@ -138,7 +141,7 @@ public class StreamCallPlugin : Plugin() {
138
141
  INITIALIZED
139
142
  }
140
143
 
141
- public fun incomingOnlyRingingConfig(): RingingConfig = object : RingingConfig {
144
+ fun incomingOnlyRingingConfig(): RingingConfig = object : RingingConfig {
142
145
  override val incomingCallSoundUri: Uri? = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
143
146
  override val outgoingCallSoundUri: Uri? = null
144
147
  }
@@ -154,83 +157,92 @@ public class StreamCallPlugin : Plugin() {
154
157
  override fun handleOnResume() {
155
158
  super.handleOnResume()
156
159
 
157
- android.util.Log.d("StreamCallPlugin", "handleOnResume: App resumed, checking permissions and pending operations")
158
- android.util.Log.d("StreamCallPlugin", "handleOnResume: Have pendingCall: ${pendingCall != null}")
159
- android.util.Log.d("StreamCallPlugin", "handleOnResume: Have pendingCallUserIds: ${pendingCallUserIds != null}")
160
- android.util.Log.d("StreamCallPlugin", "handleOnResume: Have pendingAcceptCall: ${pendingAcceptCall != null}")
161
- android.util.Log.d("StreamCallPlugin", "handleOnResume: Permission attempt count: $permissionAttemptCount")
160
+ Log.d("StreamCallPlugin", "handleOnResume: App resumed, checking permissions and pending operations")
161
+ Log.d("StreamCallPlugin", "handleOnResume: Have pendingCall: ${pendingCall != null}")
162
+ Log.d("StreamCallPlugin", "handleOnResume: Have pendingCallUserIds: ${pendingCallUserIds != null}")
163
+ Log.d("StreamCallPlugin", "handleOnResume: Have pendingAcceptCall: ${pendingAcceptCall != null}")
164
+ Log.d("StreamCallPlugin", "handleOnResume: Permission attempt count: $permissionAttemptCount")
162
165
 
163
166
  // Check if permissions were granted after returning from settings or permission dialog
164
- if (checkPermissions()) {
165
- android.util.Log.d("StreamCallPlugin", "handleOnResume: Permissions are now granted")
167
+ if (checkPermissions(this.callIsAudioOnly)) {
168
+ Log.d("StreamCallPlugin", "handleOnResume: Permissions are now granted")
166
169
  // Handle any pending calls that were waiting for permissions
167
170
  handlePermissionGranted()
168
171
  } else if (pendingCall != null || pendingAcceptCall != null) {
169
- android.util.Log.d("StreamCallPlugin", "handleOnResume: Permissions still not granted, but have pending operations")
172
+ Log.d("StreamCallPlugin", "handleOnResume: Permissions still not granted, but have pending operations")
170
173
  // If we have pending operations but permissions are still not granted,
171
174
  // it means the permission dialog was dismissed without granting
172
175
  // We should trigger our retry logic if we haven't exhausted attempts
173
176
  if (permissionAttemptCount > 0) {
174
- android.util.Log.d("StreamCallPlugin", "handleOnResume: Permission dialog was dismissed, treating as denial (attempt: $permissionAttemptCount)")
177
+ Log.d("StreamCallPlugin", "handleOnResume: Permission dialog was dismissed, treating as denial (attempt: $permissionAttemptCount)")
175
178
  val timeSinceRequest = System.currentTimeMillis() - permissionRequestStartTime
176
179
  handlePermissionDenied(timeSinceRequest)
177
180
  } else {
178
- android.util.Log.d("StreamCallPlugin", "handleOnResume: No permission attempts yet, starting permission request")
181
+ Log.d("StreamCallPlugin", "handleOnResume: No permission attempts yet, starting permission request")
179
182
  // If we have pending operations but no attempts yet, start the permission flow
180
183
  if (pendingAcceptCall != null) {
181
- android.util.Log.d("StreamCallPlugin", "handleOnResume: Have active call waiting for permissions, requesting now")
184
+ Log.d("StreamCallPlugin", "handleOnResume: Have active call waiting for permissions, requesting now")
182
185
  permissionAttemptCount = 0
183
- requestPermissions()
186
+ requestPermissions(this.callIsAudioOnly)
184
187
  } else if (pendingCall != null && pendingCallUserIds != null) {
185
- android.util.Log.d("StreamCallPlugin", "handleOnResume: Have outgoing call waiting for permissions, requesting now")
188
+ Log.d("StreamCallPlugin", "handleOnResume: Have outgoing call waiting for permissions, requesting now")
186
189
  permissionAttemptCount = 0
187
- requestPermissions()
190
+ requestPermissions(this.callIsAudioOnly)
188
191
  }
189
192
  }
190
193
  } else {
191
- android.util.Log.d("StreamCallPlugin", "handleOnResume: No pending operations, nothing to handle")
194
+ Log.d("StreamCallPlugin", "handleOnResume: No pending operations, nothing to handle")
192
195
  }
193
196
  }
194
197
 
195
198
  override fun load() {
199
+ try {
200
+ val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
201
+ if (packageInfo.firstInstallTime == packageInfo.lastUpdateTime) {
202
+ Log.d("StreamCallPlugin", "Fresh install detected, clearing user credentials.")
203
+ SecureUserRepository.getInstance(context).removeCurrentUser()
204
+ }
205
+ } catch (e: Exception) {
206
+ Log.e("StreamCallPlugin", "Error checking for fresh install", e)
207
+ }
196
208
  // general init
197
209
  initializeStreamVideo()
198
210
  setupViews()
199
211
  super.load()
200
- checkPermissions()
212
+ checkPermissions(this.callIsAudioOnly)
201
213
  // Register broadcast receiver for ACCEPT_CALL action with high priority
202
214
  val filter = IntentFilter("io.getstream.video.android.action.ACCEPT_CALL")
203
215
  filter.priority = 999 // Set high priority to ensure it captures the intent
204
- androidx.core.content.ContextCompat.registerReceiver(activity, acceptCallReceiver, filter, androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED)
205
- android.util.Log.d("StreamCallPlugin", "Registered broadcast receiver for ACCEPT_CALL action with high priority")
216
+ ContextCompat.registerReceiver(activity, acceptCallReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
217
+ Log.d("StreamCallPlugin", "Registered broadcast receiver for ACCEPT_CALL action with high priority")
206
218
 
207
219
  // Start the background service to keep the app alive
208
220
  val serviceIntent = Intent(activity, StreamCallBackgroundService::class.java)
209
221
  activity.startService(serviceIntent)
210
- android.util.Log.d("StreamCallPlugin", "Started StreamCallBackgroundService to keep app alive")
222
+ Log.d("StreamCallPlugin", "Started StreamCallBackgroundService to keep app alive")
211
223
  }
212
224
 
213
225
  @OptIn(DelicateCoroutinesApi::class)
214
- override fun handleOnNewIntent(intent: android.content.Intent) {
215
- android.util.Log.d("StreamCallPlugin", "handleOnNewIntent called: action=${intent.action}, data=${intent.data}, extras=${intent.extras}")
226
+ override fun handleOnNewIntent(intent: Intent) {
227
+ Log.d("StreamCallPlugin", "handleOnNewIntent called: action=${intent.action}, data=${intent.data}, extras=${intent.extras}")
216
228
  super.handleOnNewIntent(intent)
217
229
 
218
230
  val action = intent.action
219
231
  val data = intent.data
220
232
  val extras = intent.extras
221
- android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: Parsed action: $action")
233
+ Log.d("StreamCallPlugin", "handleOnNewIntent: Parsed action: $action")
222
234
 
223
235
  if (action === "io.getstream.video.android.action.INCOMING_CALL") {
224
- android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: Matched INCOMING_CALL action")
236
+ Log.d("StreamCallPlugin", "handleOnNewIntent: Matched INCOMING_CALL action")
225
237
  // We need to make sure the activity is visible on locked screen in such case
226
238
  changeActivityAsVisibleOnLockScreen(this@StreamCallPlugin.activity, true)
227
239
  activity?.runOnUiThread {
228
240
  val cid = intent.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)
229
- android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - Extracted cid: $cid")
241
+ Log.d("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - Extracted cid: $cid")
230
242
  if (cid != null) {
231
- android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - cid is not null, processing.")
243
+ Log.d("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - cid is not null, processing.")
232
244
  val call = streamVideoClient?.call(id = cid.id, type = cid.type)
233
- android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - Got call object: ${call?.id}")
245
+ Log.d("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - Got call object: ${call?.id}")
234
246
 
235
247
  // Try to get caller information from the call
236
248
  kotlinx.coroutines.GlobalScope.launch {
@@ -239,15 +251,15 @@ public class StreamCallPlugin : Plugin() {
239
251
  val callerInfo = callInfo?.getOrNull()?.call?.createdBy
240
252
  val custom = callInfo?.getOrNull()?.call?.custom
241
253
 
242
- val payload = com.getcapacitor.JSObject().apply {
254
+ val payload = JSObject().apply {
243
255
  put("cid", cid.cid)
244
256
  put("type", "incoming")
245
257
  if (callerInfo != null) {
246
- val caller = com.getcapacitor.JSObject().apply {
258
+ val caller = JSObject().apply {
247
259
  put("userId", callerInfo.id)
248
260
  put("name", callerInfo.name ?: "")
249
261
  put("imageURL", callerInfo.image ?: "")
250
- put("role", callerInfo.role ?: "")
262
+ put("role", callerInfo.role)
251
263
  }
252
264
  put("caller", caller)
253
265
  }
@@ -263,9 +275,9 @@ public class StreamCallPlugin : Plugin() {
263
275
  kotlinx.coroutines.delay(500) // 500ms delay
264
276
  bringAppToForeground()
265
277
  } catch (e: Exception) {
266
- android.util.Log.e("StreamCallPlugin", "Error getting call info for incoming call", e)
278
+ Log.e("StreamCallPlugin", "Error getting call info for incoming call", e)
267
279
  // Fallback to basic payload without caller info
268
- val payload = com.getcapacitor.JSObject().apply {
280
+ val payload = JSObject().apply {
269
281
  put("cid", cid.cid)
270
282
  put("type", "incoming")
271
283
  }
@@ -277,41 +289,43 @@ public class StreamCallPlugin : Plugin() {
277
289
  }
278
290
  }
279
291
  } else {
280
- android.util.Log.w("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - cid is null. Cannot process.")
292
+ Log.w("StreamCallPlugin", "handleOnNewIntent: INCOMING_CALL - cid is null. Cannot process.")
281
293
  }
282
294
  }
283
295
  } else if (action === "io.getstream.video.android.action.ACCEPT_CALL") {
284
- android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: Matched ACCEPT_CALL action")
296
+ Log.d("StreamCallPlugin", "handleOnNewIntent: Matched ACCEPT_CALL action")
285
297
  val cid = intent.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)
286
- android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: ACCEPT_CALL - Extracted cid: $cid")
298
+ Log.d("StreamCallPlugin", "handleOnNewIntent: ACCEPT_CALL - Extracted cid: $cid")
287
299
  if (cid != null) {
288
- android.util.Log.d("StreamCallPlugin", "handleOnNewIntent: ACCEPT_CALL - Accepting call with cid: $cid")
300
+ Log.d("StreamCallPlugin", "handleOnNewIntent: ACCEPT_CALL - Accepting call with cid: $cid")
289
301
  val call = streamVideoClient?.call(id = cid.id, type = cid.type)
290
302
  if (call != null) {
291
303
  // Log the full stack trace to see exactly where this is called from
292
304
  val stackTrace = Thread.currentThread().stackTrace
293
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall STACK TRACE:")
305
+ Log.d("StreamCallPlugin", "internalAcceptCall STACK TRACE:")
294
306
  stackTrace.forEachIndexed { index, element ->
295
- android.util.Log.d("StreamCallPlugin", " [$index] ${element.className}.${element.methodName}(${element.fileName}:${element.lineNumber})")
307
+ Log.d("StreamCallPlugin", " [$index] ${element.className}.${element.methodName}(${element.fileName}:${element.lineNumber})")
296
308
  }
297
309
  kotlinx.coroutines.GlobalScope.launch {
298
- internalAcceptCall(call, requestPermissionsAfter = !checkPermissions())
310
+ val isAudioOnly = getIsAudioOnly(call)
311
+ this@StreamCallPlugin.callIsAudioOnly = isAudioOnly
312
+ internalAcceptCall(call, requestPermissionsAfter = !checkPermissions(isAudioOnly))
299
313
  }
300
314
  bringAppToForeground()
301
315
  } else {
302
- android.util.Log.e("StreamCallPlugin", "handleOnNewIntent: ACCEPT_CALL - Call object is null for cid: $cid")
316
+ Log.e("StreamCallPlugin", "handleOnNewIntent: ACCEPT_CALL - Call object is null for cid: $cid")
303
317
  }
304
318
  }
305
319
  }
306
320
  // Log the intent information
307
- android.util.Log.d("StreamCallPlugin", "New Intent - Action: $action")
308
- android.util.Log.d("StreamCallPlugin", "New Intent - Data: $data")
309
- android.util.Log.d("StreamCallPlugin", "New Intent - Extras: $extras")
321
+ Log.d("StreamCallPlugin", "New Intent - Action: $action")
322
+ Log.d("StreamCallPlugin", "New Intent - Data: $data")
323
+ Log.d("StreamCallPlugin", "New Intent - Extras: $extras")
310
324
  }
311
325
 
312
326
  @OptIn(DelicateCoroutinesApi::class)
313
327
  private fun declineCall(call: Call) {
314
- android.util.Log.d("StreamCallPlugin", "declineCall called for call: ${call.id}")
328
+ Log.d("StreamCallPlugin", "declineCall called for call: ${call.id}")
315
329
  kotlinx.coroutines.GlobalScope.launch {
316
330
  try {
317
331
  call.reject()
@@ -322,7 +336,7 @@ public class StreamCallPlugin : Plugin() {
322
336
 
323
337
  hideIncomingCall()
324
338
  } catch (e: Exception) {
325
- android.util.Log.e("StreamCallPlugin", "Error declining call: ${e.message}")
339
+ Log.e("StreamCallPlugin", "Error declining call: ${e.message}")
326
340
  }
327
341
  }
328
342
  }
@@ -333,18 +347,6 @@ public class StreamCallPlugin : Plugin() {
333
347
  }
334
348
  }
335
349
 
336
- private fun showBarrier() {
337
- activity?.runOnUiThread {
338
- barrierView?.isVisible = true
339
- }
340
- }
341
-
342
- private fun hideBarrier() {
343
- activity?.runOnUiThread {
344
- barrierView?.isVisible = false
345
- }
346
- }
347
-
348
350
  @OptIn(InternalStreamVideoApi::class)
349
351
  private fun setupViews() {
350
352
  val context = context
@@ -356,7 +358,7 @@ public class StreamCallPlugin : Plugin() {
356
358
  if (rootParent != null && indexInRoot >= 0) {
357
359
  rootParent.removeViewAt(indexInRoot)
358
360
  touchInterceptWrapper = TouchInterceptWrapper(originalParent).apply {
359
- setBackgroundColor(android.graphics.Color.TRANSPARENT)
361
+ setBackgroundColor(Color.TRANSPARENT)
360
362
  }
361
363
  rootParent.addView(touchInterceptWrapper, indexInRoot)
362
364
  }
@@ -386,7 +388,7 @@ public class StreamCallPlugin : Plugin() {
386
388
  ViewGroup.LayoutParams.MATCH_PARENT,
387
389
  ViewGroup.LayoutParams.MATCH_PARENT
388
390
  )
389
- setBackgroundColor(Color.parseColor("#1a242c"))
391
+ setBackgroundColor("#1a242c".toColorInt())
390
392
  }
391
393
  parent.addView(barrierView, parent.indexOfChild(bridge?.webView) + 1) // Add above WebView
392
394
  }
@@ -400,17 +402,6 @@ public class StreamCallPlugin : Plugin() {
400
402
  VideoTheme {
401
403
  val activeCall = call ?: streamVideoClient?.state?.activeCall?.collectAsState()?.value
402
404
  if (activeCall != null) {
403
- val participants by activeCall.state.participants.collectAsStateWithLifecycle()
404
- val sortedParticipants by activeCall.state.sortedParticipants.collectAsStateWithLifecycle(emptyList())
405
- val callParticipants by remember(participants) {
406
- derivedStateOf {
407
- if (sortedParticipants.size > 6) {
408
- sortedParticipants
409
- } else {
410
- participants
411
- }
412
- }
413
- }
414
405
 
415
406
  val currentLocal by activeCall.state.me.collectAsStateWithLifecycle()
416
407
 
@@ -489,7 +480,7 @@ public class StreamCallPlugin : Plugin() {
489
480
  SecureUserRepository.getInstance(context).save(credentials)
490
481
 
491
482
  // Initialize Stream Video with new credentials
492
- if (!hadSavedCredentials || (savedCredentials!!.user.id != userId)) {
483
+ if (!hadSavedCredentials || (savedCredentials.user.id != userId)) {
493
484
  initializeStreamVideo()
494
485
  }
495
486
 
@@ -532,10 +523,10 @@ public class StreamCallPlugin : Plugin() {
532
523
  }
533
524
 
534
525
  @OptIn(DelicateCoroutinesApi::class)
535
- public fun initializeStreamVideo(passedContext: Context? = null, passedApplication: Application? = null) {
536
- android.util.Log.d("StreamCallPlugin", "initializeStreamVideo called")
526
+ fun initializeStreamVideo(passedContext: Context? = null, passedApplication: Application? = null) {
527
+ Log.d("StreamCallPlugin", "initializeStreamVideo called")
537
528
  if (state == State.INITIALIZING) {
538
- android.util.Log.v("StreamCallPlugin", "Returning, already in the process of initializing")
529
+ Log.v("StreamCallPlugin", "Returning, already in the process of initializing")
539
530
  return
540
531
  }
541
532
  state = State.INITIALIZING
@@ -548,7 +539,7 @@ public class StreamCallPlugin : Plugin() {
548
539
  // Try to get user credentials from repository
549
540
  val savedCredentials = SecureUserRepository.getInstance(contextToUse).loadCurrentUser()
550
541
  if (savedCredentials == null) {
551
- android.util.Log.v("StreamCallPlugin", "Saved credentials are null")
542
+ Log.v("StreamCallPlugin", "Saved credentials are null")
552
543
  state = State.NOT_INITIALIZED
553
544
  return
554
545
  }
@@ -556,14 +547,14 @@ public class StreamCallPlugin : Plugin() {
556
547
  try {
557
548
  // Check if we can reuse existing StreamVideo singleton client
558
549
  if (StreamVideo.isInstalled) {
559
- android.util.Log.v("StreamCallPlugin", "Found existing StreamVideo singleton client")
550
+ Log.v("StreamCallPlugin", "Found existing StreamVideo singleton client")
560
551
  if (streamVideoClient == null) {
561
- android.util.Log.v("StreamCallPlugin", "Plugin's streamVideoClient is null, reusing singleton and registering event handlers")
552
+ Log.v("StreamCallPlugin", "Plugin's streamVideoClient is null, reusing singleton and registering event handlers")
562
553
  streamVideoClient = StreamVideo.instance()
563
554
  // Register event handlers since streamVideoClient was null
564
555
  registerEventHandlers()
565
556
  } else {
566
- android.util.Log.v("StreamCallPlugin", "Plugin already has streamVideoClient, skipping event handler registration")
557
+ Log.v("StreamCallPlugin", "Plugin already has streamVideoClient, skipping event handler registration")
567
558
  }
568
559
  state = State.INITIALIZED
569
560
  initializationTime = System.currentTimeMillis()
@@ -571,11 +562,11 @@ public class StreamCallPlugin : Plugin() {
571
562
  }
572
563
 
573
564
  // If we reach here, we need to create a new client
574
- android.util.Log.v("StreamCallPlugin", "No existing StreamVideo singleton client, creating new one")
565
+ Log.v("StreamCallPlugin", "No existing StreamVideo singleton client, creating new one")
575
566
 
576
567
  // unsafe cast, add better handling
577
568
  val application = contextToUse.applicationContext as Application
578
- android.util.Log.d("StreamCallPlugin", "No existing StreamVideo singleton client, creating new one")
569
+ Log.d("StreamCallPlugin", "No existing StreamVideo singleton client, creating new one")
579
570
  val notificationHandler = CustomNotificationHandler(
580
571
  application = application,
581
572
  endCall = { callId ->
@@ -583,13 +574,13 @@ public class StreamCallPlugin : Plugin() {
583
574
 
584
575
  kotlinx.coroutines.GlobalScope.launch {
585
576
  try {
586
- android.util.Log.i(
577
+ Log.i(
587
578
  "StreamCallPlugin",
588
579
  "Attempt to endCallRaw, activeCall == null: ${activeCall == null}",
589
580
  )
590
581
  activeCall?.let { endCallRaw(it) }
591
582
  } catch (e: Exception) {
592
- android.util.Log.e(
583
+ Log.e(
593
584
  "StreamCallPlugin",
594
585
  "Error ending after missed call notif action",
595
586
  e
@@ -603,12 +594,12 @@ public class StreamCallPlugin : Plugin() {
603
594
  val now = System.currentTimeMillis()
604
595
  val isWithinOneSecond = (now - contextCreatedAt) <= 1000L
605
596
 
606
- android.util.Log.i(
597
+ Log.i(
607
598
  "StreamCallPlugin",
608
599
  "Time between context creation and activity created (incoming call notif): ${now - contextCreatedAt}"
609
600
  )
610
601
  if (isWithinOneSecond && !bootedToHandleCall) {
611
- android.util.Log.i(
602
+ Log.i(
612
603
  "StreamCallPlugin",
613
604
  "Notification incomingCall received less than 1 second after the creation of streamVideoSDK. Booted FOR SURE in order to handle the notification"
614
605
  )
@@ -644,7 +635,7 @@ public class StreamCallPlugin : Plugin() {
644
635
 
645
636
  // don't do event handler registration when activity may be null
646
637
  if (passedContext != null) {
647
- android.util.Log.w("StreamCallPlugin", "Ignoring event listeners for initializeStreamVideo")
638
+ Log.w("StreamCallPlugin", "Ignoring event listeners for initializeStreamVideo")
648
639
  passedApplication?.let {
649
640
  registerActivityEventListener(it)
650
641
  }
@@ -655,7 +646,7 @@ public class StreamCallPlugin : Plugin() {
655
646
 
656
647
  registerEventHandlers()
657
648
 
658
- android.util.Log.v("StreamCallPlugin", "Initialization finished")
649
+ Log.v("StreamCallPlugin", "Initialization finished")
659
650
  initializationTime = System.currentTimeMillis()
660
651
  state = State.INITIALIZED
661
652
  } catch (e: Exception) {
@@ -667,7 +658,7 @@ public class StreamCallPlugin : Plugin() {
667
658
  private fun moveAllActivitiesToBackgroundOrKill(context: Context, allowKill: Boolean = false) {
668
659
  try {
669
660
  if (allowKill && bootedToHandleCall && savedActivity != null) {
670
- android.util.Log.d("StreamCallPlugin", "App was booted to handle call and allowKill is true, killing app")
661
+ Log.d("StreamCallPlugin", "App was booted to handle call and allowKill is true, killing app")
671
662
  savedActivity?.let { act ->
672
663
  try {
673
664
  // Get the ActivityManager
@@ -687,7 +678,7 @@ public class StreamCallPlugin : Plugin() {
687
678
  android.os.Process.killProcess(android.os.Process.myPid())
688
679
  }, 100)
689
680
  } catch (e: Exception) {
690
- android.util.Log.e("StreamCallPlugin", "Error during aggressive cleanup", e)
681
+ Log.e("StreamCallPlugin", "Error during aggressive cleanup", e)
691
682
  // Fallback to direct process kill
692
683
  android.os.Process.killProcess(android.os.Process.myPid())
693
684
  }
@@ -695,19 +686,20 @@ public class StreamCallPlugin : Plugin() {
695
686
  return
696
687
  }
697
688
 
698
- val intent = android.content.Intent(android.content.Intent.ACTION_MAIN).apply {
699
- addCategory(android.content.Intent.CATEGORY_HOME)
689
+ val intent = Intent(Intent.ACTION_MAIN).apply {
690
+ addCategory(Intent.CATEGORY_HOME)
700
691
  flags = Intent.FLAG_ACTIVITY_NEW_TASK
701
692
  }
702
693
  context.startActivity(intent)
703
- android.util.Log.d("StreamCallPlugin", "Moving app to background using HOME intent")
694
+ Log.d("StreamCallPlugin", "Moving app to background using HOME intent")
704
695
  } catch (e: Exception) {
705
- android.util.Log.e("StreamCallPlugin", "Failed to move app to background", e)
696
+ Log.e("StreamCallPlugin", "Failed to move app to background", e)
706
697
  }
707
698
  }
708
699
 
709
700
  @OptIn(DelicateCoroutinesApi::class)
710
701
  private fun registerEventHandlers() {
702
+ Log.d("StreamCallPlugin", "registerEventHandlers called")
711
703
  eventSubscription?.dispose()
712
704
  activeCallStateJob?.cancel()
713
705
  cameraStatusJob?.cancel()
@@ -715,7 +707,7 @@ public class StreamCallPlugin : Plugin() {
715
707
  // Subscribe to call events
716
708
  streamVideoClient?.let { client ->
717
709
  eventSubscription = client.subscribe { event: VideoEvent ->
718
- android.util.Log.v("StreamCallPlugin", "Received an event ${event.getEventType()} $event")
710
+ Log.v("StreamCallPlugin", "Received an event ${event.getEventType()} $event")
719
711
  when (event) {
720
712
  is CallRingEvent -> {
721
713
  // Extract caller information from the ringing call
@@ -736,7 +728,7 @@ public class StreamCallPlugin : Plugin() {
736
728
  "userId" to callerInfo.id,
737
729
  "name" to (callerInfo.name ?: ""),
738
730
  "imageURL" to (callerInfo.image ?: ""),
739
- "role" to (callerInfo.role ?: "")
731
+ "role" to (callerInfo.role)
740
732
  )
741
733
  updateCallStatusAndNotify(event.callCid, "ringing", null, null, null, caller)
742
734
  } else {
@@ -746,7 +738,7 @@ public class StreamCallPlugin : Plugin() {
746
738
  updateCallStatusAndNotify(event.callCid, "ringing")
747
739
  }
748
740
  } catch (e: Exception) {
749
- android.util.Log.e("StreamCallPlugin", "Error getting caller info for ringing event", e)
741
+ Log.e("StreamCallPlugin", "Error getting caller info for ringing event", e)
750
742
  updateCallStatusAndNotify(event.callCid, "ringing")
751
743
  }
752
744
  }
@@ -754,9 +746,9 @@ public class StreamCallPlugin : Plugin() {
754
746
  // Handle CallCreatedEvent differently - only log it but don't try to access members yet
755
747
  is CallCreatedEvent -> {
756
748
  val callCid = event.callCid
757
- android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Received for $callCid")
758
- android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: All members from event: ${event.members.joinToString { it.user.id + " (role: " + it.user.role + ")" }}")
759
- android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Self user ID from SDK: ${this@StreamCallPlugin.streamVideoClient?.userId}")
749
+ Log.d("StreamCallPlugin", "CallCreatedEvent: Received for $callCid")
750
+ Log.d("StreamCallPlugin", "CallCreatedEvent: All members from event: ${event.members.joinToString { it.user.id + " (role: " + it.user.role + ")" }}")
751
+ Log.d("StreamCallPlugin", "CallCreatedEvent: Self user ID from SDK: ${this@StreamCallPlugin.streamVideoClient?.userId}")
760
752
 
761
753
  // Only send "created" event for outgoing calls (calls created by current user)
762
754
  // For incoming calls, we'll only send "ringing" event in CallRingEvent handler
@@ -771,21 +763,21 @@ public class StreamCallPlugin : Plugin() {
771
763
  val createdBy = callInfo?.getOrNull()?.call?.createdBy
772
764
  val currentUserId = streamVideoClient?.userId
773
765
 
774
- android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Call created by: ${createdBy?.id}, Current user: $currentUserId")
766
+ Log.d("StreamCallPlugin", "CallCreatedEvent: Call created by: ${createdBy?.id}, Current user: $currentUserId")
775
767
 
776
768
  // Only notify for outgoing calls (where current user is the creator)
777
769
  if (createdBy?.id == currentUserId) {
778
- android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: This is an outgoing call, sending created event")
770
+ Log.d("StreamCallPlugin", "CallCreatedEvent: This is an outgoing call, sending created event")
779
771
 
780
772
  val callParticipants = event.members.filter {
781
773
  val selfId = this@StreamCallPlugin.streamVideoClient?.userId
782
774
  val memberId = it.user.id
783
775
  val isSelf = memberId == selfId
784
- android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Filtering member $memberId. Self ID: $selfId. Is self: $isSelf")
776
+ Log.d("StreamCallPlugin", "CallCreatedEvent: Filtering member $memberId. Self ID: $selfId. Is self: $isSelf")
785
777
  !isSelf
786
778
  }.map { it.user.id }
787
779
 
788
- android.util.Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} remote participants: ${callParticipants.joinToString()}.")
780
+ Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} remote participants: ${callParticipants.joinToString()}.")
789
781
 
790
782
  // Start tracking this call now that we have the member list
791
783
  startCallTimeoutMonitor(callCid, callParticipants)
@@ -796,19 +788,19 @@ public class StreamCallPlugin : Plugin() {
796
788
  "userId" to member.user.id,
797
789
  "name" to (member.user.name ?: ""),
798
790
  "imageURL" to (member.user.image ?: ""),
799
- "role" to (member.user.role ?: "")
791
+ "role" to (member.user.role)
800
792
  )
801
793
  }
802
794
 
803
795
  updateCallStatusAndNotify(callCid, "created", null, null, allMembers)
804
796
  } else {
805
- android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: This is an incoming call (created by ${createdBy?.id}), not sending created event")
797
+ Log.d("StreamCallPlugin", "CallCreatedEvent: This is an incoming call (created by ${createdBy?.id}), not sending created event")
806
798
  }
807
799
  } else {
808
- android.util.Log.w("StreamCallPlugin", "CallCreatedEvent: Invalid call CID format: $callCid")
800
+ Log.w("StreamCallPlugin", "CallCreatedEvent: Invalid call CID format: $callCid")
809
801
  }
810
802
  } catch (e: Exception) {
811
- android.util.Log.e("StreamCallPlugin", "Error processing CallCreatedEvent", e)
803
+ Log.e("StreamCallPlugin", "Error processing CallCreatedEvent", e)
812
804
  }
813
805
  }
814
806
  }
@@ -844,7 +836,7 @@ public class StreamCallPlugin : Plugin() {
844
836
 
845
837
  val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
846
838
  if (keyguardManager.isKeyguardLocked) {
847
- android.util.Log.d("StreamCallPlugin", "Stop ringing and move to background")
839
+ Log.d("StreamCallPlugin", "Stop ringing and move to background")
848
840
  moveAllActivitiesToBackgroundOrKill(context)
849
841
  }
850
842
 
@@ -863,7 +855,7 @@ public class StreamCallPlugin : Plugin() {
863
855
  callState.participantResponses[userId] = "accepted"
864
856
 
865
857
  // Since someone accepted, cancel the timeout timer
866
- android.util.Log.d("StreamCallPlugin", "Call accepted by $userId, canceling timeout timer for $callCid")
858
+ Log.d("StreamCallPlugin", "Call accepted by $userId, canceling timeout timer for $callCid")
867
859
  callState.timer?.removeCallbacksAndMessages(null)
868
860
  callState.timer = null
869
861
  }
@@ -873,7 +865,7 @@ public class StreamCallPlugin : Plugin() {
873
865
 
874
866
  is CallEndedEvent -> {
875
867
  runOnMainThread {
876
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallEndedEvent for call ${event.callCid}")
868
+ Log.d("StreamCallPlugin", "Setting overlay invisible due to CallEndedEvent for call ${event.callCid}")
877
869
  // Clean up call resources
878
870
  val callCid = event.callCid
879
871
  cleanupCall(callCid)
@@ -883,7 +875,7 @@ public class StreamCallPlugin : Plugin() {
883
875
 
884
876
  is CallSessionEndedEvent -> {
885
877
  runOnMainThread {
886
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallSessionEndedEvent for call ${event.callCid}. Test session: ${event.call.session?.endedAt}")
878
+ Log.d("StreamCallPlugin", "Setting overlay invisible due to CallSessionEndedEvent for call ${event.callCid}. Test session: ${event.call.session?.endedAt}")
887
879
  // Clean up call resources
888
880
  val callCid = event.callCid
889
881
  cleanupCall(callCid)
@@ -907,23 +899,23 @@ public class StreamCallPlugin : Plugin() {
907
899
  }
908
900
  }
909
901
 
910
- android.util.Log.d("StreamCallPlugin", "CallSessionParticipantLeftEvent: Received for call $callId. Active call: ${activeCall?.cid}")
902
+ Log.d("StreamCallPlugin", "CallSessionParticipantLeftEvent: Received for call $callId. Active call: ${activeCall?.cid}")
911
903
 
912
904
 
913
905
  if (activeCall != null && activeCall.cid == callId) {
914
906
  val connectionState = activeCall.state.connection.value
915
907
  if (connectionState != RealtimeConnection.Disconnected) {
916
908
  val total = activeCall.state.participantCounts.value?.total
917
- android.util.Log.d("StreamCallPlugin", "CallSessionParticipantLeftEvent: Participant left, remaining: $total");
909
+ Log.d("StreamCallPlugin", "CallSessionParticipantLeftEvent: Participant left, remaining: $total")
918
910
  if (total != null && total <= 1) {
919
- android.util.Log.d("StreamCallPlugin", "CallSessionParticipantLeftEvent: All remote participants have left call ${activeCall.cid}. Ending call.")
911
+ Log.d("StreamCallPlugin", "CallSessionParticipantLeftEvent: All remote participants have left call ${activeCall.cid}. Ending call.")
920
912
  kotlinx.coroutines.GlobalScope.launch(Dispatchers.IO) {
921
913
  endCallRaw(activeCall)
922
914
  }
923
915
  }
924
916
  }
925
917
  } else {
926
- android.util.Log.d("StreamCallPlugin", "CallSessionParticipantLeftEvent: Conditions not met (activeCall null, or cid mismatch, or local user not joined). ActiveCall CID: ${activeCall?.cid}")
918
+ Log.d("StreamCallPlugin", "CallSessionParticipantLeftEvent: Conditions not met (activeCall null, or cid mismatch, or local user not joined). ActiveCall CID: ${activeCall?.cid}")
927
919
  }
928
920
  }
929
921
 
@@ -940,13 +932,13 @@ public class StreamCallPlugin : Plugin() {
940
932
  // used so that it follows the same patterns as iOS
941
933
  activeCallStateJob = kotlinx.coroutines.GlobalScope.launch {
942
934
  client.state.activeCall.collect { call ->
943
- android.util.Log.d("StreamCallPlugin", "Call State Update:")
944
- android.util.Log.d("StreamCallPlugin", "- Call is null: ${call == null}")
935
+ Log.d("StreamCallPlugin", "Call State Update:")
936
+ Log.d("StreamCallPlugin", "- Call is null: ${call == null}")
945
937
 
946
938
  call?.state?.let { state ->
947
- android.util.Log.d("StreamCallPlugin", "- Session ID: ${state.session.value?.id}")
948
- android.util.Log.d("StreamCallPlugin", "- All participants: ${state.participants}")
949
- android.util.Log.d("StreamCallPlugin", "- Remote participants: ${state.remoteParticipants}")
939
+ Log.d("StreamCallPlugin", "- Session ID: ${state.session.value?.id}")
940
+ Log.d("StreamCallPlugin", "- All participants: ${state.participants}")
941
+ Log.d("StreamCallPlugin", "- Remote participants: ${state.remoteParticipants}")
950
942
 
951
943
  // Notify that a call has started or state updated (e.g., participants changed but still active)
952
944
  // The actual check for "last participant" is now handled by CallSessionParticipantLeftEvent
@@ -960,7 +952,7 @@ public class StreamCallPlugin : Plugin() {
960
952
  // Listen to camera status changes
961
953
  cameraStatusJob = kotlinx.coroutines.GlobalScope.launch {
962
954
  call.camera.isEnabled.collect { isEnabled ->
963
- android.util.Log.d("StreamCallPlugin", "Camera status changed for call ${call.id}: enabled=$isEnabled")
955
+ Log.d("StreamCallPlugin", "Camera status changed for call ${call.id}: enabled=$isEnabled")
964
956
  updateCallStatusAndNotify(call.cid, if (isEnabled) "camera_enabled" else "camera_disabled")
965
957
  }
966
958
  }
@@ -968,7 +960,7 @@ public class StreamCallPlugin : Plugin() {
968
960
  // Listen to microphone status changes
969
961
  microphoneStatusJob = kotlinx.coroutines.GlobalScope.launch {
970
962
  call.microphone.isEnabled.collect { isEnabled ->
971
- android.util.Log.d("StreamCallPlugin", "Microphone status changed for call ${call.id}: enabled=$isEnabled")
963
+ Log.d("StreamCallPlugin", "Microphone status changed for call ${call.id}: enabled=$isEnabled")
972
964
  updateCallStatusAndNotify(call.cid, if (isEnabled) "microphone_enabled" else "microphone_disabled")
973
965
  }
974
966
  }
@@ -985,18 +977,18 @@ public class StreamCallPlugin : Plugin() {
985
977
  }
986
978
 
987
979
  private fun registerActivityEventListener(application: Application) {
988
- android.util.Log.i("StreamCallPlugin", "Registering activity event listener")
980
+ Log.i("StreamCallPlugin", "Registering activity event listener")
989
981
  application.registerActivityLifecycleCallbacks(object: ActivityLifecycleCallbacks() {
990
982
  override fun onActivityCreated(activity: Activity, bunlde: Bundle?) {
991
- android.util.Log.d("StreamCallPlugin", "onActivityCreated called")
983
+ Log.d("StreamCallPlugin", "onActivityCreated called")
992
984
  savedContext?.let {
993
985
  if (this@StreamCallPlugin.savedActivity != null && activity is BridgeActivity) {
994
- android.util.Log.d("StreamCallPlugin", "Activity created before, but got re-created. saving and returning")
995
- this@StreamCallPlugin.savedActivity = activity;
986
+ Log.d("StreamCallPlugin", "Activity created before, but got re-created. saving and returning")
987
+ this@StreamCallPlugin.savedActivity = activity
996
988
  return
997
989
  }
998
990
  if (initializationTime == 0L) {
999
- android.util.Log.w("StreamCallPlugin", "initializationTime is zero. Not continuing with onActivityCreated")
991
+ Log.w("StreamCallPlugin", "initializationTime is zero. Not continuing with onActivityCreated")
1000
992
  return
1001
993
  }
1002
994
 
@@ -1004,8 +996,8 @@ public class StreamCallPlugin : Plugin() {
1004
996
  val isLocked = keyguardManager.isKeyguardLocked
1005
997
 
1006
998
  if (isLocked) {
1007
- this@StreamCallPlugin.bootedToHandleCall = true;
1008
- android.util.Log.d("StreamCallPlugin", "Detected that the app booted an activity while locked. We will kill after the call fails")
999
+ this@StreamCallPlugin.bootedToHandleCall = true
1000
+ Log.d("StreamCallPlugin", "Detected that the app booted an activity while locked. We will kill after the call fails")
1009
1001
  }
1010
1002
 
1011
1003
  if (this@StreamCallPlugin.bridge == null && activity is BridgeActivity) {
@@ -1027,7 +1019,7 @@ public class StreamCallPlugin : Plugin() {
1027
1019
  this@StreamCallPlugin.savedActivityPaused = false
1028
1020
  }
1029
1021
  for (call in this@StreamCallPlugin.savedCallsToEndOnResume) {
1030
- android.util.Log.d("StreamCallPlugin", "Trying to end call with ID ${call.id} on resume")
1022
+ Log.d("StreamCallPlugin", "Trying to end call with ID ${call.id} on resume")
1031
1023
  transEndCallRaw(call)
1032
1024
  }
1033
1025
  super.onActivityResumed(activity)
@@ -1036,38 +1028,28 @@ public class StreamCallPlugin : Plugin() {
1036
1028
  }
1037
1029
 
1038
1030
  @PluginMethod
1039
- public fun acceptCall(call: PluginCall) {
1040
- android.util.Log.d("StreamCallPlugin", "acceptCall called")
1041
- try {
1042
- val streamVideoCall = streamVideoClient?.state?.ringingCall?.value
1043
- if (streamVideoCall == null) {
1044
- call.reject("Ringing call is null")
1045
- return
1046
- }
1047
-
1048
- android.util.Log.d("StreamCallPlugin", "acceptCall: Accepting call immediately, will handle permissions after")
1049
-
1050
- // Accept call immediately regardless of permissions - time is critical!
1031
+ fun acceptCall(call: PluginCall) {
1032
+ val ringingCall = streamVideoClient?.state?.ringingCall?.value
1033
+ if (ringingCall != null) {
1051
1034
  kotlinx.coroutines.GlobalScope.launch {
1052
1035
  try {
1053
- internalAcceptCall(streamVideoCall, requestPermissionsAfter = !checkPermissions())
1036
+ val isAudioOnly = getIsAudioOnly(ringingCall)
1037
+ internalAcceptCall(ringingCall, requestPermissionsAfter = !checkPermissions(isAudioOnly))
1054
1038
  call.resolve(JSObject().apply {
1055
1039
  put("success", true)
1056
1040
  })
1057
1041
  } catch (e: Exception) {
1058
- android.util.Log.e("StreamCallPlugin", "Error accepting call", e)
1059
1042
  call.reject("Failed to accept call: ${e.message}")
1060
1043
  }
1061
1044
  }
1062
- } catch (t: Throwable) {
1063
- android.util.Log.d("StreamCallPlugin", "JS -> acceptCall fail", t);
1064
- call.reject("Cannot acceptCall")
1045
+ } else {
1046
+ call.reject("No ringing call")
1065
1047
  }
1066
1048
  }
1067
1049
 
1068
1050
  @PluginMethod
1069
- public fun rejectCall(call: PluginCall) {
1070
- android.util.Log.d("StreamCallPlugin", "rejectCall called")
1051
+ fun rejectCall(call: PluginCall) {
1052
+ Log.d("StreamCallPlugin", "rejectCall called")
1071
1053
  try {
1072
1054
  val streamVideoCall = streamVideoClient?.state?.ringingCall?.value
1073
1055
  if (streamVideoCall == null) {
@@ -1078,103 +1060,106 @@ public class StreamCallPlugin : Plugin() {
1078
1060
  declineCall(streamVideoCall)
1079
1061
  }
1080
1062
  } catch (t: Throwable) {
1081
- android.util.Log.d("StreamCallPlugin", "JS -> rejectCall fail", t);
1063
+ Log.d("StreamCallPlugin", "JS -> rejectCall fail", t)
1082
1064
  call.reject("Cannot rejectCall")
1083
1065
  }
1084
1066
  }
1085
1067
 
1086
1068
  @OptIn(DelicateCoroutinesApi::class, InternalStreamVideoApi::class)
1087
1069
  internal fun internalAcceptCall(call: Call, requestPermissionsAfter: Boolean = false) {
1088
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Entered for call: ${call.id}, requestPermissionsAfter: $requestPermissionsAfter")
1070
+ Log.d("StreamCallPlugin", "internalAcceptCall: Entered for call: ${call.id}, requestPermissionsAfter: $requestPermissionsAfter")
1089
1071
 
1090
1072
  kotlinx.coroutines.GlobalScope.launch {
1091
1073
  try {
1092
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Coroutine started for call ${call.id}")
1074
+ val isAudioOnly = getIsAudioOnly(call)
1075
+ this@StreamCallPlugin.callIsAudioOnly = isAudioOnly
1076
+
1077
+ Log.d("StreamCallPlugin", "internalAcceptCall: Coroutine started for call ${call.id}")
1093
1078
 
1094
1079
  // Hide incoming call view first
1095
1080
  runOnMainThread {
1096
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Hiding incoming call view for call ${call.id}")
1081
+ Log.d("StreamCallPlugin", "internalAcceptCall: Hiding incoming call view for call ${call.id}")
1097
1082
  // No dedicated incoming-call native view anymore; UI handled by web layer
1098
1083
  }
1099
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Incoming call view hidden for call ${call.id}")
1084
+ Log.d("StreamCallPlugin", "internalAcceptCall: Incoming call view hidden for call ${call.id}")
1100
1085
 
1101
1086
  // Accept and join call immediately - don't wait for permissions!
1102
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Accepting call immediately for ${call.id}")
1087
+ Log.d("StreamCallPlugin", "internalAcceptCall: Accepting call immediately for ${call.id}")
1103
1088
  call.accept()
1104
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: call.accept() completed for call ${call.id}")
1089
+ Log.d("StreamCallPlugin", "internalAcceptCall: call.accept() completed for call ${call.id}")
1105
1090
  call.join()
1106
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: call.join() completed for call ${call.id}")
1091
+ Log.d("StreamCallPlugin", "internalAcceptCall: call.join() completed for call ${call.id}")
1107
1092
  streamVideoClient?.state?.setActiveCall(call)
1108
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: setActiveCall completed for call ${call.id}")
1093
+ Log.d("StreamCallPlugin", "internalAcceptCall: setActiveCall completed for call ${call.id}")
1109
1094
 
1110
1095
  // Notify that call has started using helper
1111
1096
  updateCallStatusAndNotify(call.id, "joined")
1112
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: updateCallStatusAndNotify(joined) called for ${call.id}")
1097
+ Log.d("StreamCallPlugin", "internalAcceptCall: updateCallStatusAndNotify(joined) called for ${call.id}")
1113
1098
 
1114
1099
  // Show overlay view with the active call and make webview transparent
1115
1100
  runOnMainThread {
1116
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Updating UI for active call ${call.id} - setting overlay visible.")
1101
+ Log.d("StreamCallPlugin", "internalAcceptCall: Updating UI for active call ${call.id} - setting overlay visible.")
1117
1102
  bridge?.webView?.setBackgroundColor(Color.TRANSPARENT) // Make webview transparent
1118
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: WebView background set to transparent for call ${call.id}")
1103
+ Log.d("StreamCallPlugin", "internalAcceptCall: WebView background set to transparent for call ${call.id}")
1119
1104
  bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
1120
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: WebView brought to front for call ${call.id}")
1121
-
1105
+ Log.d("StreamCallPlugin", "internalAcceptCall: WebView brought to front for call ${call.id}")
1106
+
1122
1107
  // Enable camera/microphone based on permissions
1123
- val hasPermissions = checkPermissions()
1124
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Has permissions: $hasPermissions for call ${call.id}")
1125
-
1126
- call.microphone?.setEnabled(hasPermissions)
1127
- call.camera?.setEnabled(hasPermissions)
1128
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Microphone and camera set to $hasPermissions for call ${call.id}")
1129
-
1130
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Setting CallContent with active call ${call.id}")
1108
+ val hasPermissions = checkPermissions(isAudioOnly)
1109
+ Log.d("StreamCallPlugin", "internalAcceptCall: Has permissions: $hasPermissions for call ${call.id}")
1110
+
1111
+ call.microphone.setEnabled(hasPermissions)
1112
+ call.camera.setEnabled(hasPermissions && !isAudioOnly)
1113
+ Log.d("StreamCallPlugin", "internalAcceptCall: Microphone and camera set to $hasPermissions for call ${call.id}")
1114
+
1115
+ Log.d("StreamCallPlugin", "internalAcceptCall: Setting CallContent with active call ${call.id}")
1131
1116
  setOverlayContent(call)
1132
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Content set for overlayView for call ${call.id}")
1117
+ Log.d("StreamCallPlugin", "internalAcceptCall: Content set for overlayView for call ${call.id}")
1133
1118
  overlayView?.isVisible = true
1134
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: OverlayView set to visible for call ${call.id}, isVisible: ${overlayView?.isVisible}")
1119
+ Log.d("StreamCallPlugin", "internalAcceptCall: OverlayView set to visible for call ${call.id}, isVisible: ${overlayView?.isVisible}")
1135
1120
 
1136
1121
  // Ensure overlay is behind WebView by adjusting its position in the parent
1137
1122
  val parent = overlayView?.parent as? ViewGroup
1138
1123
  parent?.removeView(overlayView)
1139
1124
  parent?.addView(overlayView, 0) // Add at index 0 to ensure it's behind other views
1140
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: OverlayView re-added to parent at index 0 for call ${call.id}")
1141
-
1125
+ Log.d("StreamCallPlugin", "internalAcceptCall: OverlayView re-added to parent at index 0 for call ${call.id}")
1126
+
1142
1127
  // Add a small delay to ensure UI refresh
1143
1128
  mainHandler.postDelayed({
1144
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Delayed UI check, overlay visible: ${overlayView?.isVisible} for call ${call.id}")
1129
+ Log.d("StreamCallPlugin", "internalAcceptCall: Delayed UI check, overlay visible: ${overlayView?.isVisible} for call ${call.id}")
1145
1130
  if (overlayView?.isVisible == true) {
1146
1131
  overlayView?.invalidate()
1147
1132
  overlayView?.requestLayout()
1148
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: UI invalidated and layout requested for call ${call.id}")
1133
+ Log.d("StreamCallPlugin", "internalAcceptCall: UI invalidated and layout requested for call ${call.id}")
1149
1134
  // Force refresh with active call from client
1150
1135
  val activeCall = streamVideoClient?.state?.activeCall?.value
1151
1136
  if (activeCall != null) {
1152
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Force refreshing CallContent with active call ${activeCall.id}")
1137
+ Log.d("StreamCallPlugin", "internalAcceptCall: Force refreshing CallContent with active call ${activeCall.id}")
1153
1138
  setOverlayContent(activeCall)
1154
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Content force refreshed for call ${activeCall.id}")
1139
+ Log.d("StreamCallPlugin", "internalAcceptCall: Content force refreshed for call ${activeCall.id}")
1155
1140
  } else {
1156
- android.util.Log.w("StreamCallPlugin", "internalAcceptCall: Active call is null during force refresh for call ${call.id}")
1141
+ Log.w("StreamCallPlugin", "internalAcceptCall: Active call is null during force refresh for call ${call.id}")
1157
1142
  }
1158
1143
  } else {
1159
- android.util.Log.w("StreamCallPlugin", "internalAcceptCall: overlayView not visible after delay for call ${call.id}")
1144
+ Log.w("StreamCallPlugin", "internalAcceptCall: overlayView not visible after delay for call ${call.id}")
1160
1145
  }
1161
1146
  }, 1000) // Increased delay to ensure all events are processed
1162
1147
  }
1163
1148
 
1164
1149
  // Request permissions after joining if needed
1165
1150
  if (requestPermissionsAfter) {
1166
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Requesting permissions after call acceptance for ${call.id}")
1151
+ Log.d("StreamCallPlugin", "internalAcceptCall: Requesting permissions after call acceptance for ${call.id}")
1167
1152
  runOnMainThread {
1168
1153
  // Store reference to the active call for enabling camera/mic later
1169
1154
  pendingAcceptCall = call
1170
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Set pendingAcceptCall to ${call.id}, resetting attempt count")
1155
+ Log.d("StreamCallPlugin", "internalAcceptCall: Set pendingAcceptCall to ${call.id}, resetting attempt count")
1171
1156
  permissionAttemptCount = 0
1172
- requestPermissions()
1157
+ requestPermissions(isAudioOnly)
1173
1158
  }
1174
1159
  }
1175
-
1160
+
1176
1161
  } catch (e: Exception) {
1177
- android.util.Log.e("StreamCallPlugin", "internalAcceptCall: Error accepting call ${call.id}: ${e.message}", e)
1162
+ Log.e("StreamCallPlugin", "internalAcceptCall: Error accepting call ${call.id}: ${e.message}", e)
1178
1163
  runOnMainThread {
1179
1164
  android.widget.Toast.makeText(
1180
1165
  context,
@@ -1187,55 +1172,62 @@ public class StreamCallPlugin : Plugin() {
1187
1172
  }
1188
1173
 
1189
1174
  // Function to check required permissions
1190
- private fun checkPermissions(): Boolean {
1191
- android.util.Log.d("StreamCallPlugin", "checkPermissions: Entered")
1175
+ private fun checkPermissions(isAudioOnly: Boolean = false): Boolean {
1176
+ Log.d("StreamCallPlugin", "checkPermissions: Entered, isAudioOnly: $isAudioOnly")
1192
1177
  val audioPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
1193
- android.util.Log.d("StreamCallPlugin", "checkPermissions: RECORD_AUDIO permission status: $audioPermission (Granted=${PackageManager.PERMISSION_GRANTED})")
1178
+ Log.d("StreamCallPlugin", "checkPermissions: RECORD_AUDIO permission status: $audioPermission (Granted=${PackageManager.PERMISSION_GRANTED})")
1179
+
1180
+ if (isAudioOnly) {
1181
+ val allGranted = audioPermission == PackageManager.PERMISSION_GRANTED
1182
+ Log.d("StreamCallPlugin", "checkPermissions: Audio only call, all permissions granted: $allGranted")
1183
+ return allGranted
1184
+ }
1185
+
1194
1186
  val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
1195
- android.util.Log.d("StreamCallPlugin", "checkPermissions: CAMERA permission status: $cameraPermission (Granted=${PackageManager.PERMISSION_GRANTED})")
1187
+ Log.d("StreamCallPlugin", "checkPermissions: CAMERA permission status: $cameraPermission (Granted=${PackageManager.PERMISSION_GRANTED})")
1196
1188
  val allGranted = audioPermission == PackageManager.PERMISSION_GRANTED && cameraPermission == PackageManager.PERMISSION_GRANTED
1197
- android.util.Log.d("StreamCallPlugin", "checkPermissions: All permissions granted: $allGranted")
1189
+ Log.d("StreamCallPlugin", "checkPermissions: All permissions granted: $allGranted")
1198
1190
  return allGranted
1199
1191
  }
1200
1192
 
1201
1193
  // Override to handle permission results
1202
1194
  override fun handleRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
1203
1195
  super.handleRequestPermissionsResult(requestCode, permissions, grantResults)
1204
- android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Entered. RequestCode: $requestCode, Attempt: $permissionAttemptCount")
1205
- android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Expected requestCode: 9001")
1196
+ Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Entered. RequestCode: $requestCode, Attempt: $permissionAttemptCount")
1197
+ Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Expected requestCode: 9001")
1206
1198
 
1207
1199
  if (requestCode == 9001) {
1208
1200
  val responseTime = System.currentTimeMillis() - permissionRequestStartTime
1209
- android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Response time: ${responseTime}ms")
1201
+ Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Response time: ${responseTime}ms")
1210
1202
 
1211
1203
  logPermissionResults(permissions, grantResults)
1212
1204
 
1213
1205
  if (grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
1214
- android.util.Log.i("StreamCallPlugin", "handleRequestPermissionsResult: All permissions GRANTED.")
1206
+ Log.i("StreamCallPlugin", "handleRequestPermissionsResult: All permissions GRANTED.")
1215
1207
  // Reset attempt count on success
1216
1208
  permissionAttemptCount = 0
1217
1209
  handlePermissionGranted()
1218
1210
  } else {
1219
- android.util.Log.e("StreamCallPlugin", "handleRequestPermissionsResult: Permissions DENIED. Attempt: $permissionAttemptCount")
1211
+ Log.e("StreamCallPlugin", "handleRequestPermissionsResult: Permissions DENIED. Attempt: $permissionAttemptCount")
1220
1212
  handlePermissionDenied(responseTime)
1221
1213
  }
1222
1214
  } else {
1223
- android.util.Log.w("StreamCallPlugin", "handleRequestPermissionsResult: Received unknown requestCode: $requestCode")
1215
+ Log.w("StreamCallPlugin", "handleRequestPermissionsResult: Received unknown requestCode: $requestCode")
1224
1216
  }
1225
1217
  }
1226
1218
 
1227
1219
  private fun logPermissionResults(permissions: Array<out String>, grantResults: IntArray) {
1228
- android.util.Log.d("StreamCallPlugin", "logPermissionResults: Logging permission results:")
1220
+ Log.d("StreamCallPlugin", "logPermissionResults: Logging permission results:")
1229
1221
  for (i in permissions.indices) {
1230
1222
  val permission = permissions[i]
1231
1223
  val grantResult = if (grantResults.size > i) grantResults[i] else -999 // -999 for safety if arrays mismatch
1232
1224
  val resultString = if (grantResult == PackageManager.PERMISSION_GRANTED) "GRANTED" else "DENIED ($grantResult)"
1233
- android.util.Log.d("StreamCallPlugin", " Permission: $permission, Result: $resultString")
1225
+ Log.d("StreamCallPlugin", " Permission: $permission, Result: $resultString")
1234
1226
  }
1235
1227
  }
1236
1228
 
1237
1229
  private fun handlePermissionGranted() {
1238
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Processing granted permissions")
1230
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Processing granted permissions")
1239
1231
 
1240
1232
  // Reset attempt count since permissions are now granted
1241
1233
  permissionAttemptCount = 0
@@ -1243,13 +1235,14 @@ public class StreamCallPlugin : Plugin() {
1243
1235
  // Determine what type of pending operation we have
1244
1236
  val hasOutgoingCall = pendingCall != null && pendingCallUserIds != null
1245
1237
  val hasActiveCallNeedingPermissions = pendingAcceptCall != null
1238
+ val hasPendingSetCamera = pendingSetCameraCall != null
1246
1239
 
1247
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: hasOutgoingCall=$hasOutgoingCall, hasActiveCallNeedingPermissions=$hasActiveCallNeedingPermissions")
1240
+ Log.d("StreamCallPlugin", "handlePermissionGranted: hasOutgoingCall=$hasOutgoingCall, hasActiveCallNeedingPermissions=$hasActiveCallNeedingPermissions, hasPendingSetCamera=$hasPendingSetCamera")
1248
1241
 
1249
1242
  when {
1250
1243
  hasOutgoingCall -> {
1251
1244
  // Outgoing call creation was waiting for permissions
1252
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Executing pending outgoing call with ${pendingCallUserIds?.size} users")
1245
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Executing pending outgoing call with ${pendingCallUserIds?.size} users")
1253
1246
  executePendingCall()
1254
1247
  }
1255
1248
 
@@ -1258,17 +1251,17 @@ public class StreamCallPlugin : Plugin() {
1258
1251
  val callToHandle = pendingAcceptCall!!
1259
1252
  val activeCall = streamVideoClient?.state?.activeCall?.value
1260
1253
 
1261
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Processing call ${callToHandle.id}")
1262
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Active call in state: ${activeCall?.id}")
1254
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Processing call ${callToHandle.id}")
1255
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Active call in state: ${activeCall?.id}")
1263
1256
 
1264
1257
  if (activeCall != null && activeCall.id == callToHandle.id) {
1265
1258
  // Call is already active - enable camera/microphone
1266
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Enabling camera/microphone for active call ${callToHandle.id}")
1259
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Enabling camera/microphone for active call ${callToHandle.id}")
1267
1260
  runOnMainThread {
1268
1261
  try {
1269
- callToHandle.microphone?.setEnabled(true)
1270
- callToHandle.camera?.setEnabled(true)
1271
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Camera and microphone enabled for call ${callToHandle.id}")
1262
+ callToHandle.microphone.setEnabled(true)
1263
+ callToHandle.camera.setEnabled(!this.callIsAudioOnly)
1264
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Camera and microphone enabled for call ${callToHandle.id}")
1272
1265
 
1273
1266
  // Show success message
1274
1267
  android.widget.Toast.makeText(
@@ -1277,13 +1270,13 @@ public class StreamCallPlugin : Plugin() {
1277
1270
  android.widget.Toast.LENGTH_SHORT
1278
1271
  ).show()
1279
1272
  } catch (e: Exception) {
1280
- android.util.Log.e("StreamCallPlugin", "Error enabling camera/microphone", e)
1273
+ Log.e("StreamCallPlugin", "Error enabling camera/microphone", e)
1281
1274
  }
1282
1275
  clearPendingCall()
1283
1276
  }
1284
1277
  } else if (pendingCall != null) {
1285
1278
  // Call not active yet - accept it (old flow, shouldn't happen with new flow)
1286
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Accepting pending incoming call ${callToHandle.id}")
1279
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Accepting pending incoming call ${callToHandle.id}")
1287
1280
  kotlinx.coroutines.GlobalScope.launch {
1288
1281
  try {
1289
1282
  internalAcceptCall(callToHandle)
@@ -1291,7 +1284,7 @@ public class StreamCallPlugin : Plugin() {
1291
1284
  put("success", true)
1292
1285
  })
1293
1286
  } catch (e: Exception) {
1294
- android.util.Log.e("StreamCallPlugin", "Error accepting call after permission grant", e)
1287
+ Log.e("StreamCallPlugin", "Error accepting call after permission grant", e)
1295
1288
  pendingCall?.reject("Failed to accept call: ${e.message}")
1296
1289
  } finally {
1297
1290
  clearPendingCall()
@@ -1299,12 +1292,12 @@ public class StreamCallPlugin : Plugin() {
1299
1292
  }
1300
1293
  } else {
1301
1294
  // Just enable camera/mic for the stored call even if not currently active
1302
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Enabling camera/microphone for stored call ${callToHandle.id}")
1295
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Enabling camera/microphone for stored call ${callToHandle.id}")
1303
1296
  runOnMainThread {
1304
1297
  try {
1305
- callToHandle.microphone?.setEnabled(true)
1306
- callToHandle.camera?.setEnabled(true)
1307
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Camera and microphone enabled for stored call ${callToHandle.id}")
1298
+ callToHandle.microphone.setEnabled(true)
1299
+ callToHandle.camera.setEnabled(!this.callIsAudioOnly)
1300
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Camera and microphone enabled for stored call ${callToHandle.id}")
1308
1301
 
1309
1302
  android.widget.Toast.makeText(
1310
1303
  context,
@@ -1312,23 +1305,49 @@ public class StreamCallPlugin : Plugin() {
1312
1305
  android.widget.Toast.LENGTH_SHORT
1313
1306
  ).show()
1314
1307
  } catch (e: Exception) {
1315
- android.util.Log.e("StreamCallPlugin", "Error enabling camera/microphone for stored call", e)
1308
+ Log.e("StreamCallPlugin", "Error enabling camera/microphone for stored call", e)
1316
1309
  }
1317
1310
  clearPendingCall()
1318
1311
  }
1319
1312
  }
1320
1313
  }
1321
1314
 
1315
+ hasPendingSetCamera -> {
1316
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Handling pending setCameraEnabled call.")
1317
+ val callToHandle = pendingSetCameraCall!!
1318
+ val activeCall = streamVideoClient?.state?.activeCall?.value
1319
+
1320
+ if (activeCall != null) {
1321
+ kotlinx.coroutines.GlobalScope.launch {
1322
+ try {
1323
+ activeCall.camera.setEnabled(true)
1324
+ this@StreamCallPlugin.callIsAudioOnly = false
1325
+ callToHandle.resolve(JSObject().apply {
1326
+ put("success", true)
1327
+ })
1328
+ } catch (e: Exception) {
1329
+ Log.e("StreamCallPlugin", "Error enabling camera after permission grant", e)
1330
+ callToHandle.reject("Failed to enable camera after permission grant: ${e.message}")
1331
+ } finally {
1332
+ clearPendingCall()
1333
+ }
1334
+ }
1335
+ } else {
1336
+ callToHandle.reject("No active call found to enable camera.")
1337
+ clearPendingCall()
1338
+ }
1339
+ }
1340
+
1322
1341
  pendingCall != null -> {
1323
1342
  // We have a pending call but unclear what type - fallback handling
1324
- android.util.Log.w("StreamCallPlugin", "handlePermissionGranted: Have pendingCall but unclear operation type")
1325
- android.util.Log.w("StreamCallPlugin", " - pendingCallUserIds: ${pendingCallUserIds != null}")
1326
- android.util.Log.w("StreamCallPlugin", " - pendingAcceptCall: ${pendingAcceptCall != null}")
1343
+ Log.w("StreamCallPlugin", "handlePermissionGranted: Have pendingCall but unclear operation type")
1344
+ Log.w("StreamCallPlugin", " - pendingCallUserIds: ${pendingCallUserIds != null}")
1345
+ Log.w("StreamCallPlugin", " - pendingAcceptCall: ${pendingAcceptCall != null}")
1327
1346
 
1328
1347
  // Try fallback to current ringing call for acceptance
1329
1348
  val ringingCall = streamVideoClient?.state?.ringingCall?.value
1330
1349
  if (ringingCall != null) {
1331
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Fallback - accepting current ringing call ${ringingCall.id}")
1350
+ Log.d("StreamCallPlugin", "handlePermissionGranted: Fallback - accepting current ringing call ${ringingCall.id}")
1332
1351
  kotlinx.coroutines.GlobalScope.launch {
1333
1352
  try {
1334
1353
  internalAcceptCall(ringingCall)
@@ -1336,43 +1355,43 @@ public class StreamCallPlugin : Plugin() {
1336
1355
  put("success", true)
1337
1356
  })
1338
1357
  } catch (e: Exception) {
1339
- android.util.Log.e("StreamCallPlugin", "Error accepting fallback call after permission grant", e)
1358
+ Log.e("StreamCallPlugin", "Error accepting fallback call after permission grant", e)
1340
1359
  pendingCall?.reject("Failed to accept call: ${e.message}")
1341
1360
  } finally {
1342
1361
  clearPendingCall()
1343
1362
  }
1344
1363
  }
1345
1364
  } else {
1346
- android.util.Log.w("StreamCallPlugin", "handlePermissionGranted: No ringing call found for fallback")
1365
+ Log.w("StreamCallPlugin", "handlePermissionGranted: No ringing call found for fallback")
1347
1366
  pendingCall?.reject("Unable to determine pending operation")
1348
1367
  clearPendingCall()
1349
1368
  }
1350
1369
  }
1351
1370
 
1352
1371
  else -> {
1353
- android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: No pending operations to handle")
1372
+ Log.d("StreamCallPlugin", "handlePermissionGranted: No pending operations to handle")
1354
1373
  }
1355
1374
  }
1356
1375
  }
1357
1376
 
1358
1377
  private fun handlePermissionDenied(responseTime: Long) {
1359
- android.util.Log.d("StreamCallPlugin", "handlePermissionDenied: Response time: ${responseTime}ms, Attempt: $permissionAttemptCount")
1378
+ Log.d("StreamCallPlugin", "handlePermissionDenied: Response time: ${responseTime}ms, Attempt: $permissionAttemptCount")
1360
1379
 
1361
1380
  // Check if the response was instant (< 500ms) indicating "don't ask again"
1362
1381
  val instantDenial = responseTime < 500
1363
- android.util.Log.d("StreamCallPlugin", "handlePermissionDenied: Instant denial detected: $instantDenial")
1382
+ Log.d("StreamCallPlugin", "handlePermissionDenied: Instant denial detected: $instantDenial")
1364
1383
 
1365
1384
  if (instantDenial) {
1366
1385
  // If it's an instant denial (don't ask again), go straight to settings dialog
1367
- android.util.Log.d("StreamCallPlugin", "handlePermissionDenied: Instant denial, showing settings dialog")
1386
+ Log.d("StreamCallPlugin", "handlePermissionDenied: Instant denial, showing settings dialog")
1368
1387
  showPermissionSettingsDialog()
1369
1388
  } else if (permissionAttemptCount < 2) {
1370
1389
  // Try asking again immediately if this is the first denial
1371
- android.util.Log.d("StreamCallPlugin", "handlePermissionDenied: First denial (attempt $permissionAttemptCount), asking again immediately")
1372
- requestPermissions() // This will increment the attempt count
1390
+ Log.d("StreamCallPlugin", "handlePermissionDenied: First denial (attempt $permissionAttemptCount), asking again immediately")
1391
+ requestPermissions(this.callIsAudioOnly) // This will increment the attempt count
1373
1392
  } else {
1374
1393
  // Second denial - show settings dialog (final ask)
1375
- android.util.Log.d("StreamCallPlugin", "handlePermissionDenied: Second denial (attempt $permissionAttemptCount), showing settings dialog (final ask)")
1394
+ Log.d("StreamCallPlugin", "handlePermissionDenied: Second denial (attempt $permissionAttemptCount), showing settings dialog (final ask)")
1376
1395
  showPermissionSettingsDialog()
1377
1396
  }
1378
1397
  }
@@ -1386,15 +1405,15 @@ public class StreamCallPlugin : Plugin() {
1386
1405
  val custom = pendingCustomObject
1387
1406
 
1388
1407
  if (call != null && userIds != null && callType != null && shouldRing != null) {
1389
- android.util.Log.d("StreamCallPlugin", "executePendingCall: Executing call with ${userIds.size} users")
1408
+ Log.d("StreamCallPlugin", "executePendingCall: Executing call with ${userIds.size} users")
1390
1409
 
1391
1410
  // Clear pending call data
1392
1411
  clearPendingCall()
1393
1412
 
1394
1413
  // Execute the call creation logic
1395
- createAndStartCall(call, userIds, callType, shouldRing, team, custom)
1414
+ createAndStartCall(call, userIds, callType, shouldRing, team, custom, this.callIsAudioOnly)
1396
1415
  } else {
1397
- android.util.Log.w("StreamCallPlugin", "executePendingCall: Missing pending call data")
1416
+ Log.w("StreamCallPlugin", "executePendingCall: Missing pending call data")
1398
1417
  call?.reject("Internal error: missing call parameters")
1399
1418
  clearPendingCall()
1400
1419
  }
@@ -1407,14 +1426,14 @@ public class StreamCallPlugin : Plugin() {
1407
1426
  pendingCallShouldRing = null
1408
1427
  pendingCallTeam = null
1409
1428
  pendingAcceptCall = null
1410
- pendingCallTeam = null
1429
+ pendingSetCameraCall = null
1411
1430
  permissionAttemptCount = 0 // Reset attempt count when clearing
1412
1431
  }
1413
1432
 
1414
1433
 
1415
1434
 
1416
1435
  @OptIn(DelicateCoroutinesApi::class, InternalStreamVideoApi::class)
1417
- private fun createAndStartCall(call: PluginCall, userIds: List<String>, callType: String, shouldRing: Boolean, team: String?, custom: JSObject?) {
1436
+ private fun createAndStartCall(call: PluginCall, userIds: List<String>, callType: String, shouldRing: Boolean, team: String?, custom: JSObject?, isAudioOnly: Boolean) {
1418
1437
  val selfUserId = streamVideoClient?.userId
1419
1438
  if (selfUserId == null) {
1420
1439
  call.reject("No self-user id found. Are you not logged in?")
@@ -1433,24 +1452,25 @@ public class StreamCallPlugin : Plugin() {
1433
1452
  // instead, which contains the actual participant list
1434
1453
 
1435
1454
 
1436
- android.util.Log.d("StreamCallPlugin", "Creating call with members...")
1455
+ Log.d("StreamCallPlugin", "Creating call with members...")
1437
1456
  // Create the call with all members
1438
1457
  val createResult = streamCall?.create(
1439
1458
  memberIds = userIds + selfUserId,
1440
1459
  custom = custom?.toMap() ?: emptyMap(),
1441
1460
  ring = shouldRing,
1442
1461
  team = team,
1462
+ video = !isAudioOnly
1443
1463
  )
1444
1464
 
1445
1465
  if (createResult?.isFailure == true) {
1446
1466
  throw (createResult.errorOrNull() ?: RuntimeException("Unknown error creating call")) as Throwable
1447
1467
  }
1448
1468
 
1449
- android.util.Log.d("StreamCallPlugin", "Setting overlay visible for outgoing call $callId")
1469
+ Log.d("StreamCallPlugin", "Setting overlay visible for outgoing call $callId")
1450
1470
  // Show overlay view
1451
1471
  activity?.runOnUiThread {
1452
1472
  streamCall?.microphone?.setEnabled(true)
1453
- streamCall?.camera?.setEnabled(true)
1473
+ streamCall?.camera?.setEnabled(!isAudioOnly)
1454
1474
 
1455
1475
  bridge?.webView?.setBackgroundColor(Color.TRANSPARENT) // Make webview transparent
1456
1476
  bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
@@ -1467,28 +1487,34 @@ public class StreamCallPlugin : Plugin() {
1467
1487
  put("success", true)
1468
1488
  })
1469
1489
  } catch (e: Exception) {
1470
- android.util.Log.e("StreamCallPlugin", "Error making call: ${e.message}")
1490
+ Log.e("StreamCallPlugin", "Error making call: ${e.message}")
1471
1491
  call.reject("Failed to make call: ${e.message}")
1472
1492
  }
1473
1493
  }
1474
1494
  }
1475
1495
 
1476
1496
  // Function to request required permissions
1477
- private fun requestPermissions() {
1497
+ private fun requestPermissions(isAudioOnly: Boolean) {
1478
1498
  permissionAttemptCount++
1479
- android.util.Log.d("StreamCallPlugin", "requestPermissions: Attempt #$permissionAttemptCount - Requesting RECORD_AUDIO and CAMERA permissions.")
1499
+ Log.d("StreamCallPlugin", "requestPermissions: Attempt #$permissionAttemptCount - Requesting permissions. isAudioOnly: $isAudioOnly")
1480
1500
 
1481
1501
  // Record timing for instant denial detection
1482
1502
  permissionRequestStartTime = System.currentTimeMillis()
1483
- android.util.Log.d("StreamCallPlugin", "requestPermissions: Starting permission request at $permissionRequestStartTime")
1503
+ Log.d("StreamCallPlugin", "requestPermissions: Starting permission request at $permissionRequestStartTime")
1484
1504
 
1505
+ val permissionsToRequest = if (isAudioOnly) {
1506
+ arrayOf(Manifest.permission.RECORD_AUDIO)
1507
+ } else {
1508
+ arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
1509
+ }
1510
+
1485
1511
  ActivityCompat.requestPermissions(
1486
1512
  activity,
1487
- arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA),
1513
+ permissionsToRequest,
1488
1514
  9001 // Use high request code to avoid Capacitor conflicts
1489
1515
  )
1490
1516
 
1491
- android.util.Log.d("StreamCallPlugin", "requestPermissions: Permission request initiated with code 9001")
1517
+ Log.d("StreamCallPlugin", "requestPermissions: Permission request initiated with code 9001 for permissions: ${permissionsToRequest.joinToString()}")
1492
1518
  }
1493
1519
 
1494
1520
  private fun showPermissionSettingsDialog() {
@@ -1502,19 +1528,19 @@ public class StreamCallPlugin : Plugin() {
1502
1528
  if (hasActiveCall) {
1503
1529
  builder.setMessage("Your call is active but camera and microphone are disabled.\n\nWould you like to open Settings to enable video and audio?")
1504
1530
  builder.setNegativeButton("Continue without") { _, _ ->
1505
- android.util.Log.d("StreamCallPlugin", "User chose to continue call without permissions")
1531
+ Log.d("StreamCallPlugin", "User chose to continue call without permissions")
1506
1532
  showPermissionRequiredMessage()
1507
1533
  }
1508
1534
  } else {
1509
1535
  builder.setMessage("To make video calls, this app needs Camera and Microphone permissions.\n\nWould you like to open Settings to enable them?")
1510
1536
  builder.setNegativeButton("Cancel") { _, _ ->
1511
- android.util.Log.d("StreamCallPlugin", "User declined to grant permissions - final rejection")
1537
+ Log.d("StreamCallPlugin", "User declined to grant permissions - final rejection")
1512
1538
  showPermissionRequiredMessage()
1513
1539
  }
1514
1540
  }
1515
1541
 
1516
1542
  builder.setPositiveButton("Open Settings") { _, _ ->
1517
- android.util.Log.d("StreamCallPlugin", "User chose to open app settings")
1543
+ Log.d("StreamCallPlugin", "User chose to open app settings")
1518
1544
  openAppSettings()
1519
1545
  // Don't reject the call yet - let them go to settings and come back
1520
1546
  }
@@ -1548,31 +1574,38 @@ public class StreamCallPlugin : Plugin() {
1548
1574
  }
1549
1575
 
1550
1576
  private fun handleFinalPermissionDenial() {
1551
- android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Processing final permission denial")
1577
+ Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Processing final permission denial")
1552
1578
 
1553
1579
  val hasOutgoingCall = pendingCall != null && pendingCallUserIds != null
1554
1580
  val hasIncomingCall = pendingCall != null && pendingAcceptCall != null
1581
+ val hasPendingSetCamera = pendingSetCameraCall != null
1555
1582
  val activeCall = streamVideoClient?.state?.activeCall?.value
1556
1583
 
1557
1584
  when {
1558
1585
  hasOutgoingCall -> {
1559
1586
  // Outgoing call that couldn't be created due to permissions
1560
- android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Rejecting outgoing call creation")
1587
+ Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Rejecting outgoing call creation")
1561
1588
  pendingCall?.reject("Permissions required for call. Please grant them.")
1562
1589
  clearPendingCall()
1563
1590
  }
1564
1591
 
1592
+ hasPendingSetCamera -> {
1593
+ Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Rejecting pending setCameraEnabled call")
1594
+ pendingSetCameraCall?.reject("Camera permission is required to enable the camera.")
1595
+ clearPendingCall()
1596
+ }
1597
+
1565
1598
  hasIncomingCall && activeCall != null && activeCall.id == pendingAcceptCall?.id -> {
1566
1599
  // Incoming call that's already active - DON'T end the call, just keep it without camera/mic
1567
- android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Incoming call already active, keeping call without camera/mic")
1600
+ Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Incoming call already active, keeping call without camera/mic")
1568
1601
 
1569
1602
  // Ensure camera and microphone are disabled since no permissions
1570
1603
  try {
1571
- activeCall.microphone?.setEnabled(false)
1572
- activeCall.camera?.setEnabled(false)
1573
- android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Disabled camera/microphone for call ${activeCall.id}")
1604
+ activeCall.microphone.setEnabled(false)
1605
+ activeCall.camera.setEnabled(false)
1606
+ Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Disabled camera/microphone for call ${activeCall.id}")
1574
1607
  } catch (e: Exception) {
1575
- android.util.Log.w("StreamCallPlugin", "handleFinalPermissionDenial: Error disabling camera/mic", e)
1608
+ Log.w("StreamCallPlugin", "handleFinalPermissionDenial: Error disabling camera/mic", e)
1576
1609
  }
1577
1610
 
1578
1611
  android.widget.Toast.makeText(
@@ -1591,13 +1624,13 @@ public class StreamCallPlugin : Plugin() {
1591
1624
 
1592
1625
  hasIncomingCall -> {
1593
1626
  // Incoming call that wasn't accepted yet (old flow)
1594
- android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Rejecting incoming call acceptance")
1627
+ Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Rejecting incoming call acceptance")
1595
1628
  pendingCall?.reject("Permissions required for call. Please grant them.")
1596
1629
  clearPendingCall()
1597
1630
  }
1598
1631
 
1599
1632
  else -> {
1600
- android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: No pending operations to handle")
1633
+ Log.d("StreamCallPlugin", "handleFinalPermissionDenial: No pending operations to handle")
1601
1634
  clearPendingCall()
1602
1635
  }
1603
1636
  }
@@ -1610,10 +1643,10 @@ public class StreamCallPlugin : Plugin() {
1610
1643
  try {
1611
1644
  val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
1612
1645
  ("package:" + activity.packageName).toUri())
1613
- intent.addCategory(Intent.CATEGORY_DEFAULT);
1614
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1646
+ intent.addCategory(Intent.CATEGORY_DEFAULT)
1647
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1615
1648
  context.startActivity(intent)
1616
- android.util.Log.d("StreamCallPlugin", "Opened app details settings (Android 11+)")
1649
+ Log.d("StreamCallPlugin", "Opened app details settings (Android 11+)")
1617
1650
 
1618
1651
  // Show toast with specific instructions
1619
1652
  runOnMainThread {
@@ -1625,17 +1658,17 @@ public class StreamCallPlugin : Plugin() {
1625
1658
  }
1626
1659
  return
1627
1660
  } catch (e: Exception) {
1628
- android.util.Log.w("StreamCallPlugin", "Failed to open app details, falling back", e)
1661
+ Log.w("StreamCallPlugin", "Failed to open app details, falling back", e)
1629
1662
  }
1630
1663
  }
1631
1664
 
1632
1665
  // Fallback for older Android versions or if the above fails
1633
- val intent = android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
1666
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
1634
1667
  data = Uri.fromParts("package", context.packageName, null)
1635
1668
  addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1636
1669
  }
1637
1670
  context.startActivity(intent)
1638
- android.util.Log.d("StreamCallPlugin", "Opened app settings via fallback")
1671
+ Log.d("StreamCallPlugin", "Opened app settings via fallback")
1639
1672
 
1640
1673
  // Show more specific instructions for older versions
1641
1674
  runOnMainThread {
@@ -1647,15 +1680,15 @@ public class StreamCallPlugin : Plugin() {
1647
1680
  }
1648
1681
 
1649
1682
  } catch (e: Exception) {
1650
- android.util.Log.e("StreamCallPlugin", "Error opening app settings", e)
1683
+ Log.e("StreamCallPlugin", "Error opening app settings", e)
1651
1684
 
1652
1685
  // Final fallback - open general settings
1653
1686
  try {
1654
- val intent = android.content.Intent(android.provider.Settings.ACTION_SETTINGS).apply {
1687
+ val intent = Intent(Settings.ACTION_SETTINGS).apply {
1655
1688
  addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
1656
1689
  }
1657
1690
  context.startActivity(intent)
1658
- android.util.Log.d("StreamCallPlugin", "Opened general settings as final fallback")
1691
+ Log.d("StreamCallPlugin", "Opened general settings as final fallback")
1659
1692
 
1660
1693
  runOnMainThread {
1661
1694
  android.widget.Toast.makeText(
@@ -1665,7 +1698,7 @@ public class StreamCallPlugin : Plugin() {
1665
1698
  ).show()
1666
1699
  }
1667
1700
  } catch (finalException: Exception) {
1668
- android.util.Log.e("StreamCallPlugin", "All settings intents failed", finalException)
1701
+ Log.e("StreamCallPlugin", "All settings intents failed", finalException)
1669
1702
  runOnMainThread {
1670
1703
  android.widget.Toast.makeText(
1671
1704
  context,
@@ -1701,11 +1734,12 @@ public class StreamCallPlugin : Plugin() {
1701
1734
  put("success", true)
1702
1735
  })
1703
1736
  } catch (e: Exception) {
1704
- android.util.Log.e("StreamCallPlugin", "Error setting microphone: ${e.message}")
1737
+ Log.e("StreamCallPlugin", "Error setting microphone: ${e.message}")
1705
1738
  call.reject("Failed to set microphone: ${e.message}")
1706
1739
  }
1707
1740
  }
1708
1741
  } catch (e: Exception) {
1742
+ Log.e("StreamCallPlugin", "Error setting microphone: ${e.message}")
1709
1743
  call.reject("StreamVideo not initialized")
1710
1744
  }
1711
1745
  }
@@ -1731,11 +1765,12 @@ public class StreamCallPlugin : Plugin() {
1731
1765
  put("enabled", enabled)
1732
1766
  })
1733
1767
  } catch (e: Exception) {
1734
- android.util.Log.e("StreamCallPlugin", "Error checking the camera status: ${e.message}")
1768
+ Log.e("StreamCallPlugin", "Error checking the camera status: ${e.message}")
1735
1769
  call.reject("Failed to check if camera is enabled: ${e.message}")
1736
1770
  }
1737
1771
  }
1738
1772
  } catch (e: Exception) {
1773
+ Log.e("StreamCallPlugin", "Error checking camera status: ${e.message}")
1739
1774
  call.reject("StreamVideo not initialized")
1740
1775
  }
1741
1776
  }
@@ -1748,55 +1783,81 @@ public class StreamCallPlugin : Plugin() {
1748
1783
  return
1749
1784
  }
1750
1785
 
1751
- try {
1752
- val activeCall = streamVideoClient?.state?.activeCall
1753
- if (activeCall == null) {
1754
- call.reject("No active call")
1755
- return
1786
+ val activeCall = streamVideoClient?.state?.activeCall?.value
1787
+ if (activeCall == null) {
1788
+ call.reject("No active call")
1789
+ return
1790
+ }
1791
+
1792
+ if (!enabled) {
1793
+ // Just disable, no permission needed
1794
+ kotlinx.coroutines.GlobalScope.launch {
1795
+ try {
1796
+ activeCall.camera.setEnabled(false)
1797
+ call.resolve(JSObject().apply {
1798
+ put("success", true)
1799
+ })
1800
+ } catch (e: Exception) {
1801
+ Log.e("StreamCallPlugin", "Error disabling camera: ${e.message}")
1802
+ call.reject("Failed to disable camera: ${e.message}")
1803
+ }
1756
1804
  }
1805
+ return
1806
+ }
1757
1807
 
1808
+ // From here, enabled is true. We need to check for permission.
1809
+ val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
1810
+ if (cameraPermission == PackageManager.PERMISSION_GRANTED) {
1811
+ // Permission is already granted
1758
1812
  kotlinx.coroutines.GlobalScope.launch {
1759
1813
  try {
1760
- activeCall.value?.camera?.setEnabled(enabled)
1814
+ activeCall.camera.setEnabled(true)
1815
+ // When we enable camera, the call is no longer audio-only
1816
+ this@StreamCallPlugin.callIsAudioOnly = false
1761
1817
  call.resolve(JSObject().apply {
1762
1818
  put("success", true)
1763
1819
  })
1764
1820
  } catch (e: Exception) {
1765
- android.util.Log.e("StreamCallPlugin", "Error setting camera: ${e.message}")
1766
- call.reject("Failed to set camera: ${e.message}")
1821
+ Log.e("StreamCallPlugin", "Error enabling camera: ${e.message}")
1822
+ call.reject("Failed to enable camera: ${e.message}")
1767
1823
  }
1768
1824
  }
1769
- } catch (e: Exception) {
1770
- call.reject("StreamVideo not initialized")
1825
+ } else {
1826
+ // Permission is not granted, request it.
1827
+ Log.d("StreamCallPlugin", "Camera permission not granted. Requesting permission.")
1828
+ this.pendingSetCameraCall = call
1829
+ // we are enabling camera, so it's not an audio only call
1830
+ requestPermissions(false)
1831
+ // The call will be resolved/rejected in the permission result handlers
1771
1832
  }
1772
1833
  }
1773
1834
 
1774
1835
  @OptIn(InternalStreamVideoApi::class)
1775
1836
  private suspend fun endCallRaw(call: Call) {
1776
1837
  val callId = call.id
1777
- android.util.Log.d("StreamCallPlugin", "Attempting to end call $callId")
1838
+ Log.d("StreamCallPlugin", "Attempting to end call $callId")
1778
1839
 
1779
1840
  try {
1780
1841
  // Get call information to make the decision
1781
1842
  val callInfo = call.get()
1782
- val callData = callInfo?.getOrNull()?.call
1843
+ val callData = callInfo.getOrNull()?.call
1783
1844
  val currentUserId = streamVideoClient?.userId
1784
1845
  val createdBy = callData?.createdBy?.id
1785
1846
  val isCreator = createdBy == currentUserId
1786
1847
 
1787
1848
  // Use call.state.totalParticipants to get participant count (as per StreamVideo Android SDK docs)
1788
- val totalParticipants = call.state.totalParticipants.value ?: 0
1789
- val shouldEndCall = isCreator || totalParticipants <= 1
1849
+ val totalParticipants = call.state.totalParticipants.value
1850
+ val shouldEndCall = isCreator || totalParticipants <= 2
1790
1851
 
1791
- android.util.Log.d("StreamCallPlugin", "Call $callId - Creator: $createdBy, CurrentUser: $currentUserId, IsCreator: $isCreator, TotalParticipants: $totalParticipants, ShouldEnd: $shouldEndCall")
1852
+ Log.d("StreamCallPlugin", "Call $callId - Creator: $createdBy, CurrentUser: $currentUserId, IsCreator: $isCreator, TotalParticipants: $totalParticipants, ShouldEnd: $shouldEndCall")
1792
1853
 
1793
1854
  if (shouldEndCall) {
1794
1855
  // End the call for everyone if I'm the creator or only 1 person
1795
- android.util.Log.d("StreamCallPlugin", "Ending call $callId for all participants (creator: $isCreator, participants: $totalParticipants)")
1856
+ Log.d("StreamCallPlugin", "Ending call $callId for all participants (creator: $isCreator, participants: $totalParticipants)")
1796
1857
  call.end()
1797
1858
  } else {
1798
1859
  // Just leave the call if there are more than 1 person and I'm not the creator
1799
- android.util.Log.d("StreamCallPlugin", "Leaving call $callId (not creator, >1 participants)")
1860
+ Log.d("StreamCallPlugin", "Leaving call $callId (not creator, >1 participants)")
1800
1861
  call.leave()
1801
1862
  }
1802
1863
 
@@ -1806,7 +1867,7 @@ public class StreamCallPlugin : Plugin() {
1806
1867
  }
1807
1868
 
1808
1869
  } catch (e: Exception) {
1809
- android.util.Log.e("StreamCallPlugin", "Error getting call info for $callId, defaulting to leave()", e)
1870
+ Log.e("StreamCallPlugin", "Error getting call info for $callId, defaulting to leave()", e)
1810
1871
  // Fallback to leave if we can't determine the call info
1811
1872
  call.leave()
1812
1873
  }
@@ -1814,19 +1875,18 @@ public class StreamCallPlugin : Plugin() {
1814
1875
  // Capture context from the overlayView
1815
1876
  val currentContext = overlayView?.context ?: this.savedContext
1816
1877
  if (currentContext == null) {
1817
- android.util.Log.w("StreamCallPlugin", "Cannot end call $callId because context is null")
1878
+ Log.w("StreamCallPlugin", "Cannot end call $callId because context is null")
1818
1879
  return
1819
1880
  }
1820
1881
 
1821
1882
  runOnMainThread {
1822
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible after ending call $callId")
1883
+ Log.d("StreamCallPlugin", "Setting overlay invisible after ending call $callId")
1823
1884
 
1824
1885
 
1825
1886
  currentContext.let { ctx ->
1826
1887
  val keyguardManager = ctx.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
1827
1888
  if (keyguardManager.isKeyguardLocked) {
1828
1889
  // we allow kill exclusively here
1829
- // the idea is that:
1830
1890
  // the 'empty' instance of this plugin class gets created in application
1831
1891
  // then, it handles a notification and setts the context (this.savedContext)
1832
1892
  // if the context is new
@@ -1838,7 +1898,7 @@ public class StreamCallPlugin : Plugin() {
1838
1898
  if (savedCapacitorActivity != null) {
1839
1899
 
1840
1900
  if (savedActivityPaused) {
1841
- android.util.Log.d("StreamCallPlugin", "Activity is paused. Adding call ${call.id} to savedCallsToEndOnResume")
1901
+ Log.d("StreamCallPlugin", "Activity is paused. Adding call ${call.id} to savedCallsToEndOnResume")
1842
1902
  savedCallsToEndOnResume.add(call)
1843
1903
  } else {
1844
1904
  transEndCallRaw(call)
@@ -1852,7 +1912,7 @@ public class StreamCallPlugin : Plugin() {
1852
1912
  bridge?.webView?.setBackgroundColor(Color.WHITE) // Restore webview opacity
1853
1913
 
1854
1914
  // Also hide incoming call view if visible
1855
- android.util.Log.d("StreamCallPlugin", "Hiding incoming call view for call $callId")
1915
+ Log.d("StreamCallPlugin", "Hiding incoming call view for call $callId")
1856
1916
  // No dedicated incoming-call native view anymore; UI handled by web layer
1857
1917
  }
1858
1918
 
@@ -1863,21 +1923,21 @@ public class StreamCallPlugin : Plugin() {
1863
1923
  private fun changeActivityAsVisibleOnLockScreen(activity: Activity, visible: Boolean) {
1864
1924
  if (visible) {
1865
1925
  // Ensure the activity is visible over the lock screen when launched via full-screen intent
1866
- android.util.Log.d("StreamCallPlugin", "Mark the mainActivity as visible on the lockscreen")
1926
+ Log.d("StreamCallPlugin", "Mark the mainActivity as visible on the lockscreen")
1867
1927
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
1868
1928
  activity.setShowWhenLocked(true)
1869
1929
  activity.setTurnScreenOn(true)
1870
1930
  } else {
1871
- activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
1931
+ activity.window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
1872
1932
  }
1873
1933
  } else {
1874
1934
  // Ensure the activity is NOT visible over the lock screen when launched via full-screen intent
1875
- android.util.Log.d("StreamCallPlugin", "Clear the flag for the mainActivity for visible on the lockscreen")
1935
+ Log.d("StreamCallPlugin", "Clear the flag for the mainActivity for visible on the lockscreen")
1876
1936
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
1877
1937
  activity.setShowWhenLocked(false)
1878
1938
  activity.setTurnScreenOn(false)
1879
1939
  } else {
1880
- activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
1940
+ activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
1881
1941
  }
1882
1942
  }
1883
1943
 
@@ -1888,21 +1948,21 @@ public class StreamCallPlugin : Plugin() {
1888
1948
  val callId = call.id
1889
1949
  val savedCapacitorActivity = savedActivity
1890
1950
  if (savedCapacitorActivity == null) {
1891
- android.util.Log.d("StreamCallPlugin", "Cannot perform transEndCallRaw for call $callId. savedCapacitorActivity is null")
1951
+ Log.d("StreamCallPlugin", "Cannot perform transEndCallRaw for call $callId. savedCapacitorActivity is null")
1892
1952
  return
1893
1953
  }
1894
- android.util.Log.d("StreamCallPlugin", "Performing a trans-instance call to end call with id $callId")
1954
+ Log.d("StreamCallPlugin", "Performing a trans-instance call to end call with id $callId")
1895
1955
  if (savedCapacitorActivity !is BridgeActivity) {
1896
- android.util.Log.e("StreamCallPlugin", "Saved activity is NOT a Capactor activity. Saved activity class: ${savedCapacitorActivity.javaClass.canonicalName}")
1956
+ Log.e("StreamCallPlugin", "Saved activity is NOT a Capactor activity. Saved activity class: ${savedCapacitorActivity.javaClass.canonicalName}")
1897
1957
  return
1898
1958
  }
1899
1959
  val plugin = savedCapacitorActivity.bridge.getPlugin("StreamCall")
1900
1960
  if (plugin == null) {
1901
- android.util.Log.e("StreamCallPlugin", "Plugin with name StreamCall not found?????")
1961
+ Log.e("StreamCallPlugin", "Plugin with name StreamCall not found?????")
1902
1962
  return
1903
1963
  }
1904
1964
  if (plugin.instance !is StreamCallPlugin) {
1905
- android.util.Log.e("StreamCallPlugin", "Plugin found, but invalid instance")
1965
+ Log.e("StreamCallPlugin", "Plugin found, but invalid instance")
1906
1966
  return
1907
1967
  }
1908
1968
 
@@ -1910,7 +1970,7 @@ public class StreamCallPlugin : Plugin() {
1910
1970
  try {
1911
1971
  (plugin.instance as StreamCallPlugin).endCallRaw(call)
1912
1972
  } catch (e: Exception) {
1913
- android.util.Log.e("StreamCallPlugin", "Error ending call on remote instance", e)
1973
+ Log.e("StreamCallPlugin", "Error ending call on remote instance", e)
1914
1974
  }
1915
1975
  }
1916
1976
  }
@@ -1925,12 +1985,12 @@ public class StreamCallPlugin : Plugin() {
1925
1985
  val callToEnd = activeCall ?: ringingCall
1926
1986
 
1927
1987
  if (callToEnd == null) {
1928
- android.util.Log.w("StreamCallPlugin", "Attempted to end call but no active or ringing call found")
1988
+ Log.w("StreamCallPlugin", "Attempted to end call but no active or ringing call found")
1929
1989
  call.reject("No active call to end")
1930
1990
  return
1931
1991
  }
1932
1992
 
1933
- android.util.Log.d("StreamCallPlugin", "Ending call: activeCall=${activeCall?.id}, ringingCall=${ringingCall?.id}, callToEnd=${callToEnd.id}")
1993
+ Log.d("StreamCallPlugin", "Ending call: activeCall=${activeCall?.id}, ringingCall=${ringingCall?.id}, callToEnd=${callToEnd.id}")
1934
1994
 
1935
1995
  kotlinx.coroutines.GlobalScope.launch {
1936
1996
  try {
@@ -1939,11 +1999,12 @@ public class StreamCallPlugin : Plugin() {
1939
1999
  put("success", true)
1940
2000
  })
1941
2001
  } catch (e: Exception) {
1942
- android.util.Log.e("StreamCallPlugin", "Error ending call: ${e.message}")
2002
+ Log.e("StreamCallPlugin", "Error ending call: ${e.message}")
1943
2003
  call.reject("Failed to end call: ${e.message}")
1944
2004
  }
1945
2005
  }
1946
2006
  } catch (e: Exception) {
2007
+ Log.e("StreamCallPlugin", "Error ending call: ${e.message}")
1947
2008
  call.reject("StreamVideo not initialized")
1948
2009
  }
1949
2010
  }
@@ -1974,20 +2035,23 @@ public class StreamCallPlugin : Plugin() {
1974
2035
  val callType = call.getString("type") ?: "default"
1975
2036
  val shouldRing = call.getBoolean("ring") ?: true
1976
2037
  val callId = java.util.UUID.randomUUID().toString()
1977
- val team = call.getString("team");
2038
+ val team = call.getString("team")
2039
+ val isAudioOnly = custom?.getBoolean("audio_only") ?: false
2040
+ this.callIsAudioOnly = isAudioOnly
1978
2041
 
1979
- android.util.Log.d("StreamCallPlugin", "Creating call:")
1980
- android.util.Log.d("StreamCallPlugin", "- Call ID: $callId")
1981
- android.util.Log.d("StreamCallPlugin", "- Call Type: $callType")
1982
- android.util.Log.d("StreamCallPlugin", "- Users: $userIds")
1983
- android.util.Log.d("StreamCallPlugin", "- Should Ring: $shouldRing")
2042
+
2043
+ Log.d("StreamCallPlugin", "Creating call:")
2044
+ Log.d("StreamCallPlugin", "- Call ID: $callId")
2045
+ Log.d("StreamCallPlugin", "- Call Type: $callType")
2046
+ Log.d("StreamCallPlugin", "- Users: $userIds")
2047
+ Log.d("StreamCallPlugin", "- Should Ring: $shouldRing")
1984
2048
  if (custom != null) {
1985
- android.util.Log.d("StreamCallPlugin", "- Custom data: $custom")
2049
+ Log.d("StreamCallPlugin", "- Custom data: $custom")
1986
2050
  }
1987
2051
 
1988
2052
  // Check permissions before creating the call
1989
- if (!checkPermissions()) {
1990
- android.util.Log.d("StreamCallPlugin", "Permissions not granted, storing call parameters and requesting permissions")
2053
+ if (!checkPermissions(isAudioOnly)) {
2054
+ Log.d("StreamCallPlugin", "Permissions not granted, storing call parameters and requesting permissions")
1991
2055
  // Store call parameters for later execution
1992
2056
  pendingCall = call
1993
2057
  pendingCallUserIds = userIds
@@ -1999,12 +2063,12 @@ public class StreamCallPlugin : Plugin() {
1999
2063
  }
2000
2064
  // Reset attempt count for new permission flow
2001
2065
  permissionAttemptCount = 0
2002
- requestPermissions()
2066
+ requestPermissions(isAudioOnly)
2003
2067
  return // Don't reject immediately, wait for permission result
2004
2068
  }
2005
2069
 
2006
2070
  // Execute call creation immediately if permissions are granted
2007
- createAndStartCall(call, userIds, callType, shouldRing, team, custom)
2071
+ createAndStartCall(call, userIds, callType, shouldRing, team, custom, isAudioOnly)
2008
2072
  } catch (e: Exception) {
2009
2073
  call.reject("Failed to make call: ${e.message}")
2010
2074
  }
@@ -2026,7 +2090,7 @@ public class StreamCallPlugin : Plugin() {
2026
2090
 
2027
2091
  callStates[callCid] = callState
2028
2092
 
2029
- android.util.Log.d("StreamCallPlugin", "Started timeout monitor for call $callCid with ${memberIds.size} members")
2093
+ Log.d("StreamCallPlugin", "Started timeout monitor for call $callCid with ${memberIds.size} members")
2030
2094
  }
2031
2095
 
2032
2096
  private fun checkCallTimeout(callCid: String) {
@@ -2036,12 +2100,12 @@ public class StreamCallPlugin : Plugin() {
2036
2100
  val elapsedSeconds = (now - callState.createdAt) / 1000
2037
2101
 
2038
2102
  if (elapsedSeconds >= 30) {
2039
- android.util.Log.d("StreamCallPlugin", "Call $callCid has timed out after $elapsedSeconds seconds")
2103
+ Log.d("StreamCallPlugin", "Call $callCid has timed out after $elapsedSeconds seconds")
2040
2104
 
2041
2105
  val hasAccepted = callState.participantResponses.values.any { it == "accepted" }
2042
2106
 
2043
2107
  if (!hasAccepted) {
2044
- android.util.Log.d("StreamCallPlugin", "No one accepted call $callCid, marking all non-responders as missed")
2108
+ Log.d("StreamCallPlugin", "No one accepted call $callCid, marking all non-responders as missed")
2045
2109
 
2046
2110
  // First, remove the timer to prevent further callbacks
2047
2111
  callState.timer?.removeCallbacksAndMessages(null)
@@ -2072,7 +2136,7 @@ public class StreamCallPlugin : Plugin() {
2072
2136
  // Notify that call has ended using helper
2073
2137
  updateCallStatusAndNotify(callCid, "ended", null, "timeout")
2074
2138
  } catch (e: Exception) {
2075
- android.util.Log.e("StreamCallPlugin", "Error ending timed out call", e)
2139
+ Log.e("StreamCallPlugin", "Error ending timed out call", e)
2076
2140
  }
2077
2141
  }
2078
2142
  }
@@ -2087,7 +2151,7 @@ public class StreamCallPlugin : Plugin() {
2087
2151
 
2088
2152
  if (callState != null) {
2089
2153
  // Ensure timer is properly canceled
2090
- android.util.Log.d("StreamCallPlugin", "Stopping timer for call: $callCid")
2154
+ Log.d("StreamCallPlugin", "Stopping timer for call: $callCid")
2091
2155
  callState.timer?.removeCallbacksAndMessages(null)
2092
2156
  callState.timer = null
2093
2157
  }
@@ -2097,13 +2161,13 @@ public class StreamCallPlugin : Plugin() {
2097
2161
 
2098
2162
  // Hide UI elements directly without setting content
2099
2163
  runOnMainThread {
2100
- android.util.Log.d("StreamCallPlugin", "Hiding UI elements for call $callCid (one-time cleanup)")
2164
+ Log.d("StreamCallPlugin", "Hiding UI elements for call $callCid (one-time cleanup)")
2101
2165
  overlayView?.isVisible = false
2102
2166
  // here we will also make sure we don't show on lock screen
2103
2167
  changeActivityAsVisibleOnLockScreen(this.activity, false)
2104
2168
  }
2105
2169
 
2106
- android.util.Log.d("StreamCallPlugin", "Cleaned up resources for ended call: $callCid")
2170
+ Log.d("StreamCallPlugin", "Cleaned up resources for ended call: $callCid")
2107
2171
  }
2108
2172
 
2109
2173
  private fun checkAllParticipantsResponded(callCid: String) {
@@ -2112,14 +2176,14 @@ public class StreamCallPlugin : Plugin() {
2112
2176
  val totalParticipants = callState.members.size
2113
2177
  val responseCount = callState.participantResponses.size
2114
2178
 
2115
- android.util.Log.d("StreamCallPlugin", "Checking responses for call $callCid: $responseCount / $totalParticipants")
2179
+ Log.d("StreamCallPlugin", "Checking responses for call $callCid: $responseCount / $totalParticipants")
2116
2180
 
2117
2181
  val allResponded = responseCount >= totalParticipants
2118
2182
  val allRejectedOrMissed = allResponded &&
2119
2183
  callState.participantResponses.values.all { it == "rejected" || it == "missed" }
2120
2184
 
2121
2185
  if (allResponded && allRejectedOrMissed) {
2122
- android.util.Log.d("StreamCallPlugin", "All participants have rejected or missed the call $callCid")
2186
+ Log.d("StreamCallPlugin", "All participants have rejected or missed the call $callCid")
2123
2187
 
2124
2188
  // Cancel the timer immediately to prevent further callbacks
2125
2189
  callState.timer?.removeCallbacksAndMessages(null)
@@ -2143,7 +2207,7 @@ public class StreamCallPlugin : Plugin() {
2143
2207
  // Notify that call has ended using helper
2144
2208
  updateCallStatusAndNotify(callCid, "ended", null, "all_rejected_or_missed")
2145
2209
  } catch (e: Exception) {
2146
- android.util.Log.e("StreamCallPlugin", "Error ending call after all rejected/missed", e)
2210
+ Log.e("StreamCallPlugin", "Error ending call after all rejected/missed", e)
2147
2211
  }
2148
2212
  }
2149
2213
  }
@@ -2153,10 +2217,10 @@ public class StreamCallPlugin : Plugin() {
2153
2217
 
2154
2218
  private suspend fun magicDeviceDelete(streamVideoClient: StreamVideo) {
2155
2219
  try {
2156
- android.util.Log.d("StreamCallPlugin", "Starting magicDeviceDelete operation")
2220
+ Log.d("StreamCallPlugin", "Starting magicDeviceDelete operation")
2157
2221
 
2158
2222
  FirebaseMessaging.getInstance().token.await()?.let {
2159
- android.util.Log.d("StreamCallPlugin", "Found firebase token")
2223
+ Log.d("StreamCallPlugin", "Found firebase token")
2160
2224
  val device = Device(
2161
2225
  id = it,
2162
2226
  pushProvider = PushProvider.FIREBASE.key,
@@ -2166,7 +2230,7 @@ public class StreamCallPlugin : Plugin() {
2166
2230
  streamVideoClient.deleteDevice(device)
2167
2231
  }
2168
2232
  } catch (e: Exception) {
2169
- android.util.Log.e("StreamCallPlugin", "Error in magicDeviceDelete", e)
2233
+ Log.e("StreamCallPlugin", "Error in magicDeviceDelete", e)
2170
2234
  }
2171
2235
  }
2172
2236
 
@@ -2231,12 +2295,12 @@ public class StreamCallPlugin : Plugin() {
2231
2295
 
2232
2296
  try {
2233
2297
  saveDynamicApiKey(apiKey)
2234
- android.util.Log.d("StreamCallPlugin", "Dynamic API key saved successfully")
2298
+ Log.d("StreamCallPlugin", "Dynamic API key saved successfully")
2235
2299
  call.resolve(JSObject().apply {
2236
2300
  put("success", true)
2237
2301
  })
2238
2302
  } catch (e: Exception) {
2239
- android.util.Log.e("StreamCallPlugin", "Error saving dynamic API key", e)
2303
+ Log.e("StreamCallPlugin", "Error saving dynamic API key", e)
2240
2304
  call.reject("Failed to save API key: ${e.message}")
2241
2305
  }
2242
2306
  }
@@ -2255,7 +2319,7 @@ public class StreamCallPlugin : Plugin() {
2255
2319
  }
2256
2320
  })
2257
2321
  } catch (e: Exception) {
2258
- android.util.Log.e("StreamCallPlugin", "Error getting dynamic API key", e)
2322
+ Log.e("StreamCallPlugin", "Error getting dynamic API key", e)
2259
2323
  call.reject("Failed to get API key: ${e.message}")
2260
2324
  }
2261
2325
  }
@@ -2263,17 +2327,9 @@ public class StreamCallPlugin : Plugin() {
2263
2327
  // Helper functions for managing dynamic API key in SharedPreferences
2264
2328
  private fun saveDynamicApiKey(apiKey: String) {
2265
2329
  val sharedPrefs = getApiKeyPreferences()
2266
- sharedPrefs.edit()
2267
- .putString(DYNAMIC_API_KEY_PREF, apiKey)
2268
- .apply()
2269
- }
2270
-
2271
- // Helper functions for managing dynamic API key in SharedPreferences
2272
- private fun clearDynamicApiKey() {
2273
- val sharedPrefs = getApiKeyPreferences()
2274
- sharedPrefs.edit()
2275
- .remove(DYNAMIC_API_KEY_PREF)
2276
- .apply()
2330
+ sharedPrefs.edit {
2331
+ putString(DYNAMIC_API_KEY_PREF, apiKey)
2332
+ }
2277
2333
  }
2278
2334
 
2279
2335
  private fun getDynamicApiKey(): String? {
@@ -2298,18 +2354,18 @@ public class StreamCallPlugin : Plugin() {
2298
2354
  // A) Check if the key exists in the custom preference
2299
2355
  val dynamicApiKey = getDynamicApiKey(context)
2300
2356
  return if (!dynamicApiKey.isNullOrEmpty() && dynamicApiKey.trim().isNotEmpty()) {
2301
- android.util.Log.d("StreamCallPlugin", "Using dynamic API key")
2357
+ Log.d("StreamCallPlugin", "Using dynamic API key")
2302
2358
  dynamicApiKey
2303
2359
  } else {
2304
2360
  // B) If not, use R.string.CAPACITOR_STREAM_VIDEO_APIKEY
2305
- android.util.Log.d("StreamCallPlugin", "Using static API key from resources")
2361
+ Log.d("StreamCallPlugin", "Using static API key from resources")
2306
2362
  context.getString(R.string.CAPACITOR_STREAM_VIDEO_APIKEY)
2307
2363
  }
2308
2364
  }
2309
2365
 
2310
2366
  // Helper method to update call status and notify listeners
2311
2367
  private fun updateCallStatusAndNotify(callId: String, state: String, userId: String? = null, reason: String? = null, members: List<Map<String, Any>>? = null, caller: Map<String, Any>? = null) {
2312
- android.util.Log.d("StreamCallPlugin", "updateCallStatusAndNotify called: callId=$callId, state=$state, userId=$userId, reason=$reason")
2368
+ Log.d("StreamCallPlugin", "updateCallStatusAndNotify called: callId=$callId, state=$state, userId=$userId, reason=$reason")
2313
2369
  // Update stored call info
2314
2370
  currentCallId = callId
2315
2371
  currentCallState = state
@@ -2351,6 +2407,13 @@ public class StreamCallPlugin : Plugin() {
2351
2407
  }
2352
2408
  }
2353
2409
 
2410
+ val eventString = data.toString()
2411
+ if (lastEventSent == eventString) {
2412
+ Log.d("StreamCallPlugin", "Duplicate event detected, not sending: $eventString")
2413
+ return
2414
+ }
2415
+ lastEventSent = eventString
2416
+
2354
2417
  // Notify listeners
2355
2418
  notifyListeners("callEvent", data)
2356
2419
  }
@@ -2359,13 +2422,18 @@ public class StreamCallPlugin : Plugin() {
2359
2422
  fun joinCall(call: PluginCall) {
2360
2423
  val fragment = callFragment
2361
2424
  if (fragment != null && fragment.getCall() != null) {
2362
- if (!checkPermissions()) {
2363
- requestPermissions()
2364
- call.reject("Permissions required for call. Please grant them.")
2365
- return
2366
- }
2425
+ val activeCall = fragment.getCall()!!
2426
+ // I need to get custom data here, which is async.
2427
+ // The method is not suspend.
2428
+ // Let's launch a coroutine
2367
2429
  CoroutineScope(Dispatchers.Main).launch {
2368
- fragment.getCall()?.join()
2430
+ val isAudioOnly = getIsAudioOnly(activeCall)
2431
+ if (!checkPermissions(isAudioOnly)) {
2432
+ requestPermissions(isAudioOnly)
2433
+ call.reject("Permissions required for call. Please grant them.")
2434
+ return@launch
2435
+ }
2436
+ activeCall.join()
2369
2437
  call.resolve()
2370
2438
  }
2371
2439
  } else {
@@ -2396,19 +2464,21 @@ public class StreamCallPlugin : Plugin() {
2396
2464
  private val acceptCallReceiver = object : BroadcastReceiver() {
2397
2465
  override fun onReceive(context: Context?, intent: Intent?) {
2398
2466
  if (intent?.action == "io.getstream.video.android.action.ACCEPT_CALL") {
2399
- android.util.Log.d("StreamCallPlugin", "BroadcastReceiver: Received broadcast with action: ${intent.action}")
2467
+ Log.d("StreamCallPlugin", "BroadcastReceiver: Received broadcast with action: ${intent.action}")
2400
2468
  val cid = intent.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)
2401
2469
  if (cid != null) {
2402
- android.util.Log.d("StreamCallPlugin", "BroadcastReceiver: ACCEPT_CALL broadcast received with cid: $cid")
2470
+ Log.d("StreamCallPlugin", "BroadcastReceiver: ACCEPT_CALL broadcast received with cid: $cid")
2403
2471
  val call = streamVideoClient?.call(id = cid.id, type = cid.type)
2404
2472
  if (call != null) {
2405
- android.util.Log.d("StreamCallPlugin", "BroadcastReceiver: Accepting call with cid: $cid")
2473
+ Log.d("StreamCallPlugin", "BroadcastReceiver: Accepting call with cid: $cid")
2406
2474
  kotlinx.coroutines.GlobalScope.launch {
2407
- internalAcceptCall(call, requestPermissionsAfter = !checkPermissions())
2475
+ val isAudioOnly = getIsAudioOnly(call)
2476
+ this@StreamCallPlugin.callIsAudioOnly = isAudioOnly
2477
+ internalAcceptCall(call, requestPermissionsAfter = !checkPermissions(isAudioOnly))
2408
2478
  }
2409
2479
  bringAppToForeground()
2410
2480
  } else {
2411
- android.util.Log.e("StreamCallPlugin", "BroadcastReceiver: Call object is null for cid: $cid")
2481
+ Log.e("StreamCallPlugin", "BroadcastReceiver: Call object is null for cid: $cid")
2412
2482
  }
2413
2483
  }
2414
2484
  }
@@ -2422,12 +2492,12 @@ public class StreamCallPlugin : Plugin() {
2422
2492
  launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP)
2423
2493
  if (launchIntent != null) {
2424
2494
  ctx.startActivity(launchIntent)
2425
- android.util.Log.d("StreamCallPlugin", "bringAppToForeground: Launch intent executed to foreground app")
2495
+ Log.d("StreamCallPlugin", "bringAppToForeground: Launch intent executed to foreground app")
2426
2496
  } else {
2427
- android.util.Log.w("StreamCallPlugin", "bringAppToForeground: launchIntent is null")
2497
+ Log.w("StreamCallPlugin", "bringAppToForeground: launchIntent is null")
2428
2498
  }
2429
2499
  } catch (e: Exception) {
2430
- android.util.Log.e("StreamCallPlugin", "bringAppToForeground error", e)
2500
+ Log.e("StreamCallPlugin", "bringAppToForeground error", e)
2431
2501
  }
2432
2502
  }
2433
2503
 
@@ -2446,4 +2516,14 @@ public class StreamCallPlugin : Plugin() {
2446
2516
  private const val API_KEY_PREFS_NAME = "stream_video_api_key_prefs"
2447
2517
  private const val DYNAMIC_API_KEY_PREF = "dynamic_api_key"
2448
2518
  }
2519
+
2520
+ private suspend fun getIsAudioOnly(call: Call): Boolean {
2521
+ val callInfoResult = call.get()
2522
+ return if (callInfoResult.isSuccess) {
2523
+ val audioOnlyValue = callInfoResult.getOrNull()?.call?.custom?.get("audio_only")
2524
+ audioOnlyValue?.toString() == "true"
2525
+ } else {
2526
+ false
2527
+ }
2528
+ }
2449
2529
  }