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