@capgo/capacitor-stream-call 0.0.2
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/Package.swift +31 -0
- package/README.md +340 -0
- package/StreamCall.podspec +19 -0
- package/android/build.gradle +74 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CallOverlayView.kt +281 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomNotificationHandler.kt +142 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/IncomingCallView.kt +147 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/RingtonePlayer.kt +164 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +1014 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/TouchInterceptWrapper.kt +31 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/UserRepository.kt +111 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/values/strings.xml +7 -0
- package/dist/docs.json +533 -0
- package/dist/esm/definitions.d.ts +169 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +32 -0
- package/dist/esm/web.js +323 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +337 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +339 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/StreamCallPlugin/CallOverlayView.swift +147 -0
- package/ios/Sources/StreamCallPlugin/CustomCallParticipantImageView.swift +60 -0
- package/ios/Sources/StreamCallPlugin/CustomCallView.swift +257 -0
- package/ios/Sources/StreamCallPlugin/CustomVideoParticipantsView.swift +107 -0
- package/ios/Sources/StreamCallPlugin/ParticipantsView.swift +206 -0
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +722 -0
- package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +177 -0
- package/ios/Sources/StreamCallPlugin/UserRepository.swift +96 -0
- package/ios/Sources/StreamCallPlugin/WebviewNavigationDelegate.swift +68 -0
- package/ios/Tests/StreamCallPluginTests/StreamCallPluginTests.swift +15 -0
- package/package.json +96 -0
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
package ee.forgr.capacitor.streamcall
|
|
2
|
+
|
|
3
|
+
import TouchInterceptWrapper
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.app.Application
|
|
6
|
+
import android.app.KeyguardManager
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.graphics.Color
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.os.Bundle
|
|
11
|
+
import android.os.Handler
|
|
12
|
+
import android.os.Looper
|
|
13
|
+
import android.view.View
|
|
14
|
+
import android.view.ViewGroup
|
|
15
|
+
import android.widget.FrameLayout
|
|
16
|
+
import androidx.compose.ui.platform.ComposeView
|
|
17
|
+
import androidx.core.view.isVisible
|
|
18
|
+
import com.getcapacitor.BridgeActivity
|
|
19
|
+
import com.getcapacitor.JSObject
|
|
20
|
+
import com.getcapacitor.Plugin
|
|
21
|
+
import com.getcapacitor.PluginCall
|
|
22
|
+
import com.getcapacitor.PluginMethod
|
|
23
|
+
import com.getcapacitor.annotation.CapacitorPlugin
|
|
24
|
+
import io.getstream.android.push.firebase.FirebasePushDeviceGenerator
|
|
25
|
+
import io.getstream.android.push.permissions.ActivityLifecycleCallbacks
|
|
26
|
+
import io.getstream.video.android.core.Call
|
|
27
|
+
import io.getstream.video.android.core.GEO
|
|
28
|
+
import io.getstream.video.android.core.RingingState
|
|
29
|
+
import io.getstream.video.android.core.StreamVideo
|
|
30
|
+
import io.getstream.video.android.core.StreamVideoBuilder
|
|
31
|
+
import io.getstream.video.android.core.model.RejectReason
|
|
32
|
+
import io.getstream.video.android.core.notifications.NotificationConfig
|
|
33
|
+
import io.getstream.video.android.core.notifications.NotificationHandler
|
|
34
|
+
import io.getstream.video.android.core.sounds.emptyRingingConfig
|
|
35
|
+
import io.getstream.video.android.core.sounds.toSounds
|
|
36
|
+
import io.getstream.video.android.model.StreamCallId
|
|
37
|
+
import io.getstream.video.android.model.User
|
|
38
|
+
import io.getstream.video.android.model.streamCallId
|
|
39
|
+
import kotlinx.coroutines.launch
|
|
40
|
+
import org.openapitools.client.models.CallEndedEvent
|
|
41
|
+
import org.openapitools.client.models.CallMissedEvent
|
|
42
|
+
import org.openapitools.client.models.CallRejectedEvent
|
|
43
|
+
import org.openapitools.client.models.CallSessionEndedEvent
|
|
44
|
+
import org.openapitools.client.models.VideoEvent
|
|
45
|
+
|
|
46
|
+
// I am not a religious pearson, but at this point, I am not sure even god himself would understand this code
|
|
47
|
+
// It's a spaghetti-like, tangled, unreadable mess and frankly, I am deeply sorry for the code crimes commited in the Android impl
|
|
48
|
+
@CapacitorPlugin(name = "StreamCall")
|
|
49
|
+
public class StreamCallPlugin : Plugin() {
|
|
50
|
+
private var streamVideoClient: StreamVideo? = null
|
|
51
|
+
private var state: State = State.NOT_INITIALIZED
|
|
52
|
+
private var overlayView: ComposeView? = null
|
|
53
|
+
private var incomingCallView: ComposeView? = null
|
|
54
|
+
private var barrierView: View? = null
|
|
55
|
+
private var ringtonePlayer: RingtonePlayer? = null
|
|
56
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
57
|
+
private var savedContext: Context? = null
|
|
58
|
+
private var bootedToHandleCall: Boolean = false
|
|
59
|
+
private var initializationTime: Long = 0
|
|
60
|
+
private var savedActivity: Activity? = null
|
|
61
|
+
private var savedActivityPaused = false
|
|
62
|
+
private var savedCallsToEndOnResume = mutableListOf<Call>()
|
|
63
|
+
|
|
64
|
+
private enum class State {
|
|
65
|
+
NOT_INITIALIZED,
|
|
66
|
+
INITIALIZING,
|
|
67
|
+
INITIALIZED
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private fun runOnMainThread(action: () -> Unit) {
|
|
71
|
+
mainHandler.post { action() }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override fun handleOnPause() {
|
|
75
|
+
this.ringtonePlayer.let { it?.pauseRinging() }
|
|
76
|
+
super.handleOnPause()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override fun handleOnResume() {
|
|
80
|
+
this.ringtonePlayer.let { it?.resumeRinging() }
|
|
81
|
+
super.handleOnResume()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override fun load() {
|
|
85
|
+
// general init
|
|
86
|
+
ringtonePlayer = RingtonePlayer(
|
|
87
|
+
this.activity.application,
|
|
88
|
+
cancelIncomingCallService = {
|
|
89
|
+
var streamVideoClient = this.streamVideoClient
|
|
90
|
+
if (streamVideoClient == null) {
|
|
91
|
+
android.util.Log.d("StreamCallPlugin", "StreamVideo SDK client is null, no incoming call notification can be constructed")
|
|
92
|
+
return@RingtonePlayer
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
val callServiceClass = Class.forName("io.getstream.video.android.core.notifications.internal.service.CallService")
|
|
97
|
+
val companionClass = callServiceClass.declaredClasses.first { it.simpleName == "Companion" }
|
|
98
|
+
// Instead of getting INSTANCE, we'll get the companion object through the enclosing class
|
|
99
|
+
val companionField = callServiceClass.getDeclaredField("Companion")
|
|
100
|
+
companionField.isAccessible = true
|
|
101
|
+
val companionInstance = companionField.get(null)
|
|
102
|
+
|
|
103
|
+
val removeIncomingCallMethod = companionClass.getDeclaredMethod(
|
|
104
|
+
"removeIncomingCall",
|
|
105
|
+
Context::class.java,
|
|
106
|
+
Class.forName("io.getstream.video.android.model.StreamCallId"),
|
|
107
|
+
Class.forName("io.getstream.video.android.core.notifications.internal.service.CallServiceConfig")
|
|
108
|
+
)
|
|
109
|
+
removeIncomingCallMethod.isAccessible = true
|
|
110
|
+
|
|
111
|
+
// Get the default config using reflection
|
|
112
|
+
val defaultConfigClass = Class.forName("io.getstream.video.android.core.notifications.internal.service.DefaultCallConfigurations")
|
|
113
|
+
val defaultField = defaultConfigClass.getDeclaredField("INSTANCE")
|
|
114
|
+
val defaultInstance = defaultField.get(null)
|
|
115
|
+
val defaultMethod = defaultConfigClass.getDeclaredMethod("getDefault")
|
|
116
|
+
val defaultConfig = defaultMethod.invoke(defaultInstance)
|
|
117
|
+
|
|
118
|
+
val app = this.activity.application
|
|
119
|
+
val cId = streamVideoClient?.state?.ringingCall?.value?.cid?.let { StreamCallId.fromCallCid(it) }
|
|
120
|
+
if (app == null || cId == null || defaultConfig == null) {
|
|
121
|
+
android.util.Log.e("StreamCallPlugin", "Some required parameters are null - app: ${app == null}, cId: ${cId == null}, defaultConfig: ${defaultConfig == null}")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Call the method
|
|
125
|
+
removeIncomingCallMethod.invoke(companionInstance, app, cId, defaultConfig)
|
|
126
|
+
} catch (e : Throwable) {
|
|
127
|
+
android.util.Log.e("StreamCallPlugin", "Reflecting streamNotificationManager and the config DID NOT work", e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
initializeStreamVideo()
|
|
132
|
+
setupViews()
|
|
133
|
+
super.load()
|
|
134
|
+
|
|
135
|
+
// Handle initial intent if present
|
|
136
|
+
activity?.intent?.let { handleOnNewIntent(it) }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
override fun handleOnNewIntent(intent: android.content.Intent) {
|
|
140
|
+
super.handleOnNewIntent(intent)
|
|
141
|
+
|
|
142
|
+
val action = intent.action
|
|
143
|
+
val data = intent.data
|
|
144
|
+
val extras = intent.extras
|
|
145
|
+
|
|
146
|
+
if (action === "io.getstream.video.android.action.INCOMING_CALL") {
|
|
147
|
+
activity?.runOnUiThread {
|
|
148
|
+
val cid = intent.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)
|
|
149
|
+
if (cid != null) {
|
|
150
|
+
val call = streamVideoClient?.call(id = cid.id, type = cid.type)
|
|
151
|
+
// Start playing ringtone
|
|
152
|
+
ringtonePlayer?.startRinging()
|
|
153
|
+
// let's set a barrier. This will prevent the user from interacting with the webview while the calling screen is loading
|
|
154
|
+
// Launch a coroutine to handle the suspend function
|
|
155
|
+
showBarrier()
|
|
156
|
+
|
|
157
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
158
|
+
call?.get()
|
|
159
|
+
activity?.runOnUiThread {
|
|
160
|
+
incomingCallView?.setContent {
|
|
161
|
+
IncomingCallView(
|
|
162
|
+
streamVideo = streamVideoClient,
|
|
163
|
+
call = call,
|
|
164
|
+
onDeclineCall = { declinedCall ->
|
|
165
|
+
declineCall(declinedCall)
|
|
166
|
+
},
|
|
167
|
+
onAcceptCall = { acceptedCall ->
|
|
168
|
+
acceptCall(acceptedCall)
|
|
169
|
+
},
|
|
170
|
+
onHideIncomingCall = {
|
|
171
|
+
hideIncomingCall()
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
incomingCallView?.isVisible = true
|
|
176
|
+
hideBarrier()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} else if (action === "io.getstream.video.android.action.ACCEPT_CALL") {
|
|
182
|
+
// it's a strategic placed initializeStreamVideo. I want to register the even listeners
|
|
183
|
+
// (which are not initialized during the first load in initialization by the application class)
|
|
184
|
+
// initializeStreamVideo()
|
|
185
|
+
val cid = intent.streamCallId(NotificationHandler.INTENT_EXTRA_CALL_CID)
|
|
186
|
+
if (cid != null) {
|
|
187
|
+
val call = streamVideoClient?.call(id = cid.id, type = cid.type)
|
|
188
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
189
|
+
call?.get()
|
|
190
|
+
call?.let { acceptCall(it) }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Log the intent information
|
|
195
|
+
android.util.Log.d("StreamCallPlugin", "New Intent - Action: $action")
|
|
196
|
+
android.util.Log.d("StreamCallPlugin", "New Intent - Data: $data")
|
|
197
|
+
android.util.Log.d("StreamCallPlugin", "New Intent - Extras: $extras")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private fun declineCall(call: Call) {
|
|
201
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
202
|
+
call.reject(RejectReason.Decline)
|
|
203
|
+
|
|
204
|
+
// Stop ringtone
|
|
205
|
+
ringtonePlayer?.stopRinging()
|
|
206
|
+
|
|
207
|
+
// Notify that call has ended
|
|
208
|
+
val data = JSObject().apply {
|
|
209
|
+
put("callId", call.id)
|
|
210
|
+
put("state", "rejected")
|
|
211
|
+
}
|
|
212
|
+
notifyListeners("callEvent", data)
|
|
213
|
+
|
|
214
|
+
hideIncomingCall()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private fun hideIncomingCall() {
|
|
219
|
+
activity?.runOnUiThread {
|
|
220
|
+
incomingCallView?.isVisible = false
|
|
221
|
+
// Stop ringtone if it's still playing
|
|
222
|
+
ringtonePlayer?.stopRinging()
|
|
223
|
+
// Check if device is locked using KeyguardManager
|
|
224
|
+
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
225
|
+
if (keyguardManager.isKeyguardLocked) {
|
|
226
|
+
activity.moveTaskToBack(true)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private fun showBarrier() {
|
|
232
|
+
activity?.runOnUiThread {
|
|
233
|
+
barrierView?.isVisible = true
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun hideBarrier() {
|
|
238
|
+
activity?.runOnUiThread {
|
|
239
|
+
barrierView?.isVisible = false
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// private fun remoteIncomingCallNotif() {
|
|
244
|
+
// CallService.removeIncomingCall(
|
|
245
|
+
// context,
|
|
246
|
+
// StreamCallId.fromCallCid(call.cid),
|
|
247
|
+
// StreamVideo.instance().state.callConfigRegistry.get(call.type),
|
|
248
|
+
// )
|
|
249
|
+
// }
|
|
250
|
+
|
|
251
|
+
private fun setupViews() {
|
|
252
|
+
val context = context
|
|
253
|
+
val parent = bridge?.webView?.parent as? ViewGroup ?: return
|
|
254
|
+
|
|
255
|
+
// Make WebView transparent
|
|
256
|
+
bridge?.webView?.setBackgroundColor(Color.TRANSPARENT)
|
|
257
|
+
|
|
258
|
+
// Create and add overlay view below WebView
|
|
259
|
+
overlayView = ComposeView(context).apply {
|
|
260
|
+
isVisible = false
|
|
261
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
262
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
263
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
264
|
+
)
|
|
265
|
+
setContent {
|
|
266
|
+
CallOverlayView(
|
|
267
|
+
context = context,
|
|
268
|
+
streamVideo = streamVideoClient,
|
|
269
|
+
call = null
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
parent.addView(overlayView, 0) // Add at index 0 to ensure it's below WebView
|
|
274
|
+
|
|
275
|
+
val originalContainer: ViewGroup = getBridge().webView
|
|
276
|
+
|
|
277
|
+
val wrapper = TouchInterceptWrapper(parent)
|
|
278
|
+
(parent.parent as ViewGroup).removeView(originalContainer)
|
|
279
|
+
(parent.parent as ViewGroup).addView(wrapper, 0)
|
|
280
|
+
|
|
281
|
+
// Create barrier view
|
|
282
|
+
barrierView = View(context).apply {
|
|
283
|
+
isVisible = false
|
|
284
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
285
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
286
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
287
|
+
)
|
|
288
|
+
setBackgroundColor(Color.parseColor("#1a242c"))
|
|
289
|
+
}
|
|
290
|
+
parent.addView(barrierView, parent.indexOfChild(bridge?.webView) + 1) // Add above WebView
|
|
291
|
+
|
|
292
|
+
// Create and add incoming call view
|
|
293
|
+
incomingCallView = ComposeView(context).apply {
|
|
294
|
+
isVisible = false
|
|
295
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
296
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
297
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
298
|
+
)
|
|
299
|
+
setContent {
|
|
300
|
+
IncomingCallView(streamVideoClient)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
parent.addView(incomingCallView, parent.indexOfChild(bridge?.webView) + 2) // Add above WebView
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@PluginMethod
|
|
307
|
+
fun login(call: PluginCall) {
|
|
308
|
+
val token = call.getString("token")
|
|
309
|
+
val userId = call.getString("userId")
|
|
310
|
+
val name = call.getString("name")
|
|
311
|
+
|
|
312
|
+
if (token == null || userId == null || name == null) {
|
|
313
|
+
call.reject("Missing required parameters: token, userId, or name")
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
val imageURL = call.getString("imageURL")
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
// Create user object
|
|
321
|
+
val user = User(
|
|
322
|
+
id = userId,
|
|
323
|
+
name = name,
|
|
324
|
+
image = imageURL,
|
|
325
|
+
custom = emptyMap() // Initialize with empty map for custom data
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
val savedCredentials = SecureUserRepository.getInstance(this.context).loadCurrentUser()
|
|
329
|
+
val hadSavedCredentials = savedCredentials != null
|
|
330
|
+
|
|
331
|
+
// Create credentials and save them
|
|
332
|
+
val credentials = UserCredentials(user, token)
|
|
333
|
+
SecureUserRepository.getInstance(context).save(credentials)
|
|
334
|
+
|
|
335
|
+
// Initialize Stream Video with new credentials
|
|
336
|
+
if (!hadSavedCredentials) {
|
|
337
|
+
initializeStreamVideo()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
val ret = JSObject()
|
|
341
|
+
ret.put("success", true)
|
|
342
|
+
call.resolve(ret)
|
|
343
|
+
} catch (e: Exception) {
|
|
344
|
+
call.reject("Failed to login", e)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@PluginMethod
|
|
349
|
+
fun logout(call: PluginCall) {
|
|
350
|
+
try {
|
|
351
|
+
// Clear stored credentials
|
|
352
|
+
SecureUserRepository.getInstance(context).removeCurrentUser()
|
|
353
|
+
|
|
354
|
+
// Properly cleanup the client
|
|
355
|
+
streamVideoClient?.let {
|
|
356
|
+
StreamVideo.removeClient()
|
|
357
|
+
}
|
|
358
|
+
streamVideoClient = null
|
|
359
|
+
state = State.NOT_INITIALIZED
|
|
360
|
+
|
|
361
|
+
val ret = JSObject()
|
|
362
|
+
ret.put("success", true)
|
|
363
|
+
call.resolve(ret)
|
|
364
|
+
} catch (e: Exception) {
|
|
365
|
+
call.reject("Failed to logout", e)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
public fun initializeStreamVideo(passedContext: Context? = null, passedApplication: Application? = null) {
|
|
370
|
+
android.util.Log.v("StreamCallPlugin", "Attempting to initialize streamVideo")
|
|
371
|
+
if (state == State.INITIALIZING) {
|
|
372
|
+
android.util.Log.v("StreamCallPlugin", "Returning, already in the process of initializing")
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
state = State.INITIALIZING
|
|
376
|
+
|
|
377
|
+
if (passedContext != null) {
|
|
378
|
+
this.savedContext = passedContext
|
|
379
|
+
}
|
|
380
|
+
val contextToUse = passedContext ?: this.context
|
|
381
|
+
|
|
382
|
+
// Try to get user credentials from repository
|
|
383
|
+
val savedCredentials = SecureUserRepository.getInstance(contextToUse).loadCurrentUser()
|
|
384
|
+
if (savedCredentials == null) {
|
|
385
|
+
android.util.Log.v("StreamCallPlugin", "Saved credentials are null")
|
|
386
|
+
state = State.NOT_INITIALIZED
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
// Check if we can reuse existing StreamVideo singleton client
|
|
392
|
+
if (StreamVideo.isInstalled) {
|
|
393
|
+
android.util.Log.v("StreamCallPlugin", "Found existing StreamVideo singleton client")
|
|
394
|
+
if (streamVideoClient == null) {
|
|
395
|
+
android.util.Log.v("StreamCallPlugin", "Plugin's streamVideoClient is null, reusing singleton and registering event handlers")
|
|
396
|
+
streamVideoClient = StreamVideo.instance()
|
|
397
|
+
// Register event handlers since streamVideoClient was null
|
|
398
|
+
registerEventHandlers()
|
|
399
|
+
} else {
|
|
400
|
+
android.util.Log.v("StreamCallPlugin", "Plugin already has streamVideoClient, skipping event handler registration")
|
|
401
|
+
}
|
|
402
|
+
state = State.INITIALIZED
|
|
403
|
+
initializationTime = System.currentTimeMillis()
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// If we reach here, we need to create a new client
|
|
408
|
+
android.util.Log.v("StreamCallPlugin", "No existing StreamVideo singleton client, creating new one")
|
|
409
|
+
|
|
410
|
+
// unsafe cast, add better handling
|
|
411
|
+
val application = contextToUse.applicationContext as Application
|
|
412
|
+
android.util.Log.d("StreamCallPlugin", "No existing StreamVideo singleton client, creating new one")
|
|
413
|
+
val notificationHandler = CustomNotificationHandler(
|
|
414
|
+
application = application,
|
|
415
|
+
endCall = { callId ->
|
|
416
|
+
val activeCall = streamVideoClient?.call(callId.type, callId.id)
|
|
417
|
+
|
|
418
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
419
|
+
try {
|
|
420
|
+
android.util.Log.i(
|
|
421
|
+
"StreamCallPlugin",
|
|
422
|
+
"Attempt to endCallRaw, activeCall == null: ${activeCall == null}",
|
|
423
|
+
)
|
|
424
|
+
activeCall?.let { endCallRaw(it) }
|
|
425
|
+
} catch (e: Exception) {
|
|
426
|
+
android.util.Log.e(
|
|
427
|
+
"StreamCallPlugin",
|
|
428
|
+
"Error ending after missed call notif action",
|
|
429
|
+
e
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
incomingCall = {
|
|
435
|
+
if (this.savedContext != null && initializationTime != 0L) {
|
|
436
|
+
val contextCreatedAt = initializationTime
|
|
437
|
+
val now = System.currentTimeMillis()
|
|
438
|
+
val isWithinOneSecond = (now - contextCreatedAt) <= 1000L
|
|
439
|
+
|
|
440
|
+
android.util.Log.i(
|
|
441
|
+
"StreamCallPlugin",
|
|
442
|
+
"Time between context creation and activity created (incoming call notif): ${now - contextCreatedAt}"
|
|
443
|
+
)
|
|
444
|
+
if (isWithinOneSecond && !bootedToHandleCall) {
|
|
445
|
+
android.util.Log.i(
|
|
446
|
+
"StreamCallPlugin",
|
|
447
|
+
"Notification incomingCall received less than 1 second after the creation of streamVideoSDK. Booted FOR SURE in order to handle the notification"
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
val notificationConfig = NotificationConfig(
|
|
455
|
+
pushDeviceGenerators = listOf(FirebasePushDeviceGenerator(
|
|
456
|
+
providerName = "firebase",
|
|
457
|
+
context = contextToUse
|
|
458
|
+
)),
|
|
459
|
+
requestPermissionOnAppLaunch = { true },
|
|
460
|
+
notificationHandler = notificationHandler,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
val soundsConfig = emptyRingingConfig()
|
|
464
|
+
soundsConfig.incomingCallSoundUri
|
|
465
|
+
// Initialize StreamVideo client
|
|
466
|
+
streamVideoClient = StreamVideoBuilder(
|
|
467
|
+
context = contextToUse,
|
|
468
|
+
apiKey = contextToUse.getString(R.string.CAPACITOR_STREAM_VIDEO_APIKEY),
|
|
469
|
+
geo = GEO.GlobalEdgeNetwork,
|
|
470
|
+
user = savedCredentials.user,
|
|
471
|
+
token = savedCredentials.tokenValue,
|
|
472
|
+
notificationConfig = notificationConfig,
|
|
473
|
+
sounds = soundsConfig.toSounds()
|
|
474
|
+
//, loggingLevel = LoggingLevel(priority = Priority.VERBOSE)
|
|
475
|
+
).build()
|
|
476
|
+
|
|
477
|
+
// don't do event handler registration when activity may be null
|
|
478
|
+
if (passedContext != null) {
|
|
479
|
+
android.util.Log.w("StreamCallPlugin", "Ignoring event listeners for initializeStreamVideo")
|
|
480
|
+
passedApplication?.let {
|
|
481
|
+
registerActivityEventListener(it)
|
|
482
|
+
}
|
|
483
|
+
initializationTime = System.currentTimeMillis()
|
|
484
|
+
this.state = State.INITIALIZED
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
registerEventHandlers()
|
|
489
|
+
|
|
490
|
+
android.util.Log.v("StreamCallPlugin", "Initialization finished")
|
|
491
|
+
initializationTime = System.currentTimeMillis()
|
|
492
|
+
state = State.INITIALIZED
|
|
493
|
+
} catch (e: Exception) {
|
|
494
|
+
state = State.NOT_INITIALIZED
|
|
495
|
+
throw e
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private fun moveAllActivitiesToBackgroundOrKill(context: Context, allowKill: Boolean = false) {
|
|
500
|
+
try {
|
|
501
|
+
if (allowKill && bootedToHandleCall && savedActivity != null) {
|
|
502
|
+
android.util.Log.d("StreamCallPlugin", "App was booted to handle call and allowKill is true, killing app")
|
|
503
|
+
savedActivity?.let { act ->
|
|
504
|
+
try {
|
|
505
|
+
// Get the ActivityManager
|
|
506
|
+
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
|
|
507
|
+
// Remove the task
|
|
508
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
509
|
+
val tasks = activityManager.appTasks
|
|
510
|
+
tasks.forEach { task ->
|
|
511
|
+
task.finishAndRemoveTask()
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Finish the activity
|
|
515
|
+
act.finish()
|
|
516
|
+
// Remove from recents
|
|
517
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
518
|
+
act.finishAndRemoveTask()
|
|
519
|
+
}
|
|
520
|
+
// Give a small delay for cleanup
|
|
521
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
522
|
+
// Kill the process
|
|
523
|
+
android.os.Process.killProcess(android.os.Process.myPid())
|
|
524
|
+
}, 100)
|
|
525
|
+
} catch (e: Exception) {
|
|
526
|
+
android.util.Log.e("StreamCallPlugin", "Error during aggressive cleanup", e)
|
|
527
|
+
// Fallback to direct process kill
|
|
528
|
+
android.os.Process.killProcess(android.os.Process.myPid())
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
val intent = android.content.Intent(android.content.Intent.ACTION_MAIN).apply {
|
|
535
|
+
addCategory(android.content.Intent.CATEGORY_HOME)
|
|
536
|
+
flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
|
537
|
+
}
|
|
538
|
+
context.startActivity(intent)
|
|
539
|
+
android.util.Log.d("StreamCallPlugin", "Moving app to background using HOME intent")
|
|
540
|
+
} catch (e: Exception) {
|
|
541
|
+
android.util.Log.e("StreamCallPlugin", "Failed to move app to background", e)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private fun registerEventHandlers() {
|
|
546
|
+
// Subscribe to call events
|
|
547
|
+
streamVideoClient?.let { client ->
|
|
548
|
+
client.subscribe { event: VideoEvent ->
|
|
549
|
+
android.util.Log.v("StreamCallPlugin", "Received an event ${event.getEventType()} $event")
|
|
550
|
+
when (event) {
|
|
551
|
+
is CallEndedEvent -> {
|
|
552
|
+
runOnMainThread {
|
|
553
|
+
android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallEndedEvent for call ${event.call.cid}")
|
|
554
|
+
overlayView?.setContent {
|
|
555
|
+
CallOverlayView(
|
|
556
|
+
context = context,
|
|
557
|
+
streamVideo = streamVideoClient,
|
|
558
|
+
call = null
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
overlayView?.isVisible = false
|
|
562
|
+
}
|
|
563
|
+
val data = JSObject().apply {
|
|
564
|
+
put("callId", event.call.cid)
|
|
565
|
+
put("state", "left")
|
|
566
|
+
}
|
|
567
|
+
notifyListeners("callEvent", data)
|
|
568
|
+
}
|
|
569
|
+
is CallSessionEndedEvent -> {
|
|
570
|
+
runOnMainThread {
|
|
571
|
+
android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallSessionEndedEvent for call ${event.call.cid}")
|
|
572
|
+
overlayView?.setContent {
|
|
573
|
+
CallOverlayView(
|
|
574
|
+
context = context,
|
|
575
|
+
streamVideo = streamVideoClient,
|
|
576
|
+
call = null
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
overlayView?.isVisible = false
|
|
580
|
+
}
|
|
581
|
+
val data = JSObject().apply {
|
|
582
|
+
put("callId", event.call.cid)
|
|
583
|
+
put("state", "left")
|
|
584
|
+
}
|
|
585
|
+
notifyListeners("callEvent", data)
|
|
586
|
+
}
|
|
587
|
+
is CallRejectedEvent -> {
|
|
588
|
+
runOnMainThread {
|
|
589
|
+
android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallRejectedEvent for call ${event.call.cid}")
|
|
590
|
+
overlayView?.setContent {
|
|
591
|
+
CallOverlayView(
|
|
592
|
+
context = context,
|
|
593
|
+
streamVideo = streamVideoClient,
|
|
594
|
+
call = null
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
overlayView?.isVisible = false
|
|
598
|
+
|
|
599
|
+
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
600
|
+
if (keyguardManager.isKeyguardLocked) {
|
|
601
|
+
android.util.Log.d("StreamCallPlugin", "Stop ringing and move to background")
|
|
602
|
+
this@StreamCallPlugin.ringtonePlayer?.stopRinging()
|
|
603
|
+
moveAllActivitiesToBackgroundOrKill(context)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
val data = JSObject().apply {
|
|
607
|
+
put("callId", event.call.cid)
|
|
608
|
+
put("state", "rejected")
|
|
609
|
+
}
|
|
610
|
+
notifyListeners("callEvent", data)
|
|
611
|
+
}
|
|
612
|
+
is CallMissedEvent -> {
|
|
613
|
+
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
614
|
+
if (keyguardManager.isKeyguardLocked) {
|
|
615
|
+
android.util.Log.d("StreamCallPlugin", "Stop ringing and move to background")
|
|
616
|
+
this.ringtonePlayer?.stopRinging()
|
|
617
|
+
moveAllActivitiesToBackgroundOrKill(context)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
val data = JSObject().apply {
|
|
622
|
+
put("callId", streamVideoClient?.state?.activeCall?.value?.cid)
|
|
623
|
+
put("state", event.getEventType())
|
|
624
|
+
}
|
|
625
|
+
notifyListeners("callEvent", data)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Add call state subscription using collect
|
|
629
|
+
// used so that it follows the same patterns as iOS
|
|
630
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
631
|
+
client.state.activeCall.collect { call ->
|
|
632
|
+
android.util.Log.d("StreamCallPlugin", "Call State Update:")
|
|
633
|
+
android.util.Log.d("StreamCallPlugin", "- Call is null: ${call == null}")
|
|
634
|
+
|
|
635
|
+
call?.state?.let { state ->
|
|
636
|
+
android.util.Log.d("StreamCallPlugin", "- Session ID: ${state.session.value?.id}")
|
|
637
|
+
android.util.Log.d("StreamCallPlugin", "- All participants: ${state.participants}")
|
|
638
|
+
android.util.Log.d("StreamCallPlugin", "- Remote participants: ${state.remoteParticipants}")
|
|
639
|
+
|
|
640
|
+
// Notify that a call has started
|
|
641
|
+
val data = JSObject().apply {
|
|
642
|
+
put("callId", call.cid)
|
|
643
|
+
put("state", "joined")
|
|
644
|
+
}
|
|
645
|
+
notifyListeners("callEvent", data)
|
|
646
|
+
} ?: run {
|
|
647
|
+
// Notify that call has ended
|
|
648
|
+
val data = JSObject().apply {
|
|
649
|
+
put("callId", "")
|
|
650
|
+
put("state", "left")
|
|
651
|
+
}
|
|
652
|
+
notifyListeners("callEvent", data)
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private fun registerActivityEventListener(application: Application) {
|
|
660
|
+
android.util.Log.i("StreamCallPlugin", "Registering activity event listener")
|
|
661
|
+
application.registerActivityLifecycleCallbacks(object: ActivityLifecycleCallbacks() {
|
|
662
|
+
override fun onActivityCreated(activity: Activity, bunlde: Bundle?) {
|
|
663
|
+
android.util.Log.d("StreamCallPlugin", "onActivityCreated called")
|
|
664
|
+
savedContext?.let {
|
|
665
|
+
if (this@StreamCallPlugin.savedActivity != null && activity is BridgeActivity) {
|
|
666
|
+
android.util.Log.d("StreamCallPlugin", "Activity created before, but got re-created. saving and returning")
|
|
667
|
+
this@StreamCallPlugin.savedActivity = activity;
|
|
668
|
+
return
|
|
669
|
+
}
|
|
670
|
+
if (initializationTime == 0L) {
|
|
671
|
+
android.util.Log.w("StreamCallPlugin", "initializationTime is zero. Not continuing with onActivityCreated")
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
val keyguardManager = application.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
676
|
+
val isLocked = keyguardManager.isKeyguardLocked
|
|
677
|
+
|
|
678
|
+
if (isLocked) {
|
|
679
|
+
this@StreamCallPlugin.bootedToHandleCall = true;
|
|
680
|
+
android.util.Log.d("StreamCallPlugin", "Detected that the app booted an activity while locked. We will kill after the call fails")
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (this@StreamCallPlugin.bridge == null && activity is BridgeActivity) {
|
|
684
|
+
this@StreamCallPlugin.savedActivity = activity
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
super.onActivityCreated(activity, bunlde)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
override fun onActivityPaused(activity: Activity) {
|
|
691
|
+
if (activity is BridgeActivity && activity == this@StreamCallPlugin.savedActivity) {
|
|
692
|
+
this@StreamCallPlugin.savedActivityPaused = true
|
|
693
|
+
}
|
|
694
|
+
super.onActivityPaused(activity)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
override fun onActivityResumed(activity: Activity) {
|
|
698
|
+
if (activity is BridgeActivity && activity == this@StreamCallPlugin.savedActivity) {
|
|
699
|
+
this@StreamCallPlugin.savedActivityPaused = false
|
|
700
|
+
}
|
|
701
|
+
for (call in this@StreamCallPlugin.savedCallsToEndOnResume) {
|
|
702
|
+
android.util.Log.d("StreamCallPlugin", "Trying to end call with ID ${call.id} on resume")
|
|
703
|
+
transEndCallRaw(call)
|
|
704
|
+
}
|
|
705
|
+
super.onActivityResumed(activity)
|
|
706
|
+
}
|
|
707
|
+
})
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private fun acceptCall(call: Call) {
|
|
711
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
712
|
+
android.util.Log.i("StreamCallPlugin", "Attempting to accept call ${call.id}")
|
|
713
|
+
try {
|
|
714
|
+
// Stop ringtone
|
|
715
|
+
ringtonePlayer?.stopRinging()
|
|
716
|
+
|
|
717
|
+
// Hide incoming call view first
|
|
718
|
+
runOnMainThread {
|
|
719
|
+
android.util.Log.d("StreamCallPlugin", "Hiding incoming call view for call ${call.id}")
|
|
720
|
+
incomingCallView?.isVisible = false
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Accept the call
|
|
724
|
+
call.accept()
|
|
725
|
+
|
|
726
|
+
// Notify that call has started
|
|
727
|
+
val data = JSObject().apply {
|
|
728
|
+
put("callId", call.id)
|
|
729
|
+
put("state", "joined")
|
|
730
|
+
}
|
|
731
|
+
notifyListeners("callEvent", data)
|
|
732
|
+
|
|
733
|
+
// Show overlay view with the active call
|
|
734
|
+
runOnMainThread {
|
|
735
|
+
android.util.Log.d("StreamCallPlugin", "Setting overlay visible after accepting call ${call.id}")
|
|
736
|
+
overlayView?.setContent {
|
|
737
|
+
CallOverlayView(
|
|
738
|
+
context = context,
|
|
739
|
+
streamVideo = streamVideoClient,
|
|
740
|
+
call = call
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
overlayView?.isVisible = true
|
|
744
|
+
}
|
|
745
|
+
} catch (e: Exception) {
|
|
746
|
+
android.util.Log.e("StreamCallPlugin", "Error accepting call ${call.id}: ${e.message}")
|
|
747
|
+
runOnMainThread {
|
|
748
|
+
android.widget.Toast.makeText(
|
|
749
|
+
context,
|
|
750
|
+
"Failed to join call: ${e.message}",
|
|
751
|
+
android.widget.Toast.LENGTH_LONG
|
|
752
|
+
).show()
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
@PluginMethod
|
|
759
|
+
fun setMicrophoneEnabled(call: PluginCall) {
|
|
760
|
+
val enabled = call.getBoolean("enabled") ?: run {
|
|
761
|
+
call.reject("Missing required parameter: enabled")
|
|
762
|
+
return
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
val activeCall = streamVideoClient?.state?.activeCall
|
|
767
|
+
if (activeCall == null) {
|
|
768
|
+
call.reject("No active call")
|
|
769
|
+
return
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
773
|
+
try {
|
|
774
|
+
activeCall.value?.microphone?.setEnabled(enabled)
|
|
775
|
+
call.resolve(JSObject().apply {
|
|
776
|
+
put("success", true)
|
|
777
|
+
})
|
|
778
|
+
} catch (e: Exception) {
|
|
779
|
+
android.util.Log.e("StreamCallPlugin", "Error setting microphone: ${e.message}")
|
|
780
|
+
call.reject("Failed to set microphone: ${e.message}")
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
} catch (e: Exception) {
|
|
784
|
+
call.reject("StreamVideo not initialized")
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
@PluginMethod
|
|
789
|
+
fun setCameraEnabled(call: PluginCall) {
|
|
790
|
+
val enabled = call.getBoolean("enabled") ?: run {
|
|
791
|
+
call.reject("Missing required parameter: enabled")
|
|
792
|
+
return
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
try {
|
|
796
|
+
val activeCall = streamVideoClient?.state?.activeCall
|
|
797
|
+
if (activeCall == null) {
|
|
798
|
+
call.reject("No active call")
|
|
799
|
+
return
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
803
|
+
try {
|
|
804
|
+
activeCall.value?.camera?.setEnabled(enabled)
|
|
805
|
+
call.resolve(JSObject().apply {
|
|
806
|
+
put("success", true)
|
|
807
|
+
})
|
|
808
|
+
} catch (e: Exception) {
|
|
809
|
+
android.util.Log.e("StreamCallPlugin", "Error setting camera: ${e.message}")
|
|
810
|
+
call.reject("Failed to set camera: ${e.message}")
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
} catch (e: Exception) {
|
|
814
|
+
call.reject("StreamVideo not initialized")
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
suspend fun endCallRaw(call: Call) {
|
|
819
|
+
val callId = call.id
|
|
820
|
+
android.util.Log.d("StreamCallPlugin", "Attempting to end call $callId")
|
|
821
|
+
call.leave()
|
|
822
|
+
call.reject(reason = RejectReason.Cancel)
|
|
823
|
+
|
|
824
|
+
// Capture context from the overlayView
|
|
825
|
+
val currentContext = overlayView?.context ?: this.savedContext
|
|
826
|
+
if (currentContext == null) {
|
|
827
|
+
android.util.Log.w("StreamCallPlugin", "Cannot end call $callId because context is null")
|
|
828
|
+
return
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
runOnMainThread {
|
|
832
|
+
android.util.Log.d("StreamCallPlugin", "Setting overlay invisible after ending call $callId")
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
currentContext.let { ctx ->
|
|
836
|
+
val keyguardManager = ctx.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
837
|
+
if (keyguardManager.isKeyguardLocked) {
|
|
838
|
+
// we allow kill exclusively here
|
|
839
|
+
// the idea is that:
|
|
840
|
+
// the 'empty' instance of this plugin class gets created in application
|
|
841
|
+
// then, it handles a notification and setts the context (this.savedContext)
|
|
842
|
+
// if the context is new
|
|
843
|
+
moveAllActivitiesToBackgroundOrKill(ctx, true)
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
var savedCapacitorActivity = savedActivity
|
|
848
|
+
if (savedCapacitorActivity != null) {
|
|
849
|
+
|
|
850
|
+
if (savedActivityPaused) {
|
|
851
|
+
android.util.Log.d("StreamCallPlugin", "Activity is paused. Adding call ${call.id} to savedCallsToEndOnResume")
|
|
852
|
+
savedCallsToEndOnResume.add(call)
|
|
853
|
+
} else {
|
|
854
|
+
transEndCallRaw(call)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return@runOnMainThread
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
overlayView?.setContent {
|
|
861
|
+
CallOverlayView(
|
|
862
|
+
context = currentContext,
|
|
863
|
+
streamVideo = streamVideoClient,
|
|
864
|
+
call = null
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
overlayView?.isVisible = false
|
|
868
|
+
this@StreamCallPlugin.ringtonePlayer?.stopRinging()
|
|
869
|
+
|
|
870
|
+
// Also hide incoming call view if visible
|
|
871
|
+
android.util.Log.d("StreamCallPlugin", "Hiding incoming call view for call $callId")
|
|
872
|
+
incomingCallView?.isVisible = false
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Notify that call has ended
|
|
876
|
+
val data = JSObject().apply {
|
|
877
|
+
put("callId", callId)
|
|
878
|
+
put("state", "left")
|
|
879
|
+
}
|
|
880
|
+
notifyListeners("callEvent", data)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private fun transEndCallRaw(call: Call) {
|
|
884
|
+
val callId = call.id
|
|
885
|
+
var savedCapacitorActivity = savedActivity
|
|
886
|
+
if (savedCapacitorActivity == null) {
|
|
887
|
+
android.util.Log.d("StreamCallPlugin", "Cannot perform transEndCallRaw for call $callId. savedCapacitorActivity is null")
|
|
888
|
+
return
|
|
889
|
+
}
|
|
890
|
+
android.util.Log.d("StreamCallPlugin", "Performing a trans-instance call to end call with id $callId")
|
|
891
|
+
if (savedCapacitorActivity !is BridgeActivity) {
|
|
892
|
+
android.util.Log.e("StreamCallPlugin", "Saved activity is NOT a Capactor activity. Saved activity class: ${savedCapacitorActivity.javaClass.canonicalName}")
|
|
893
|
+
return
|
|
894
|
+
}
|
|
895
|
+
val plugin = savedCapacitorActivity.bridge.getPlugin("StreamCall")
|
|
896
|
+
if (plugin == null) {
|
|
897
|
+
android.util.Log.e("StreamCallPlugin", "Plugin with name StreamCall not found?????")
|
|
898
|
+
return
|
|
899
|
+
}
|
|
900
|
+
if (plugin.instance !is StreamCallPlugin) {
|
|
901
|
+
android.util.Log.e("StreamCallPlugin", "Plugin found, but invalid instance")
|
|
902
|
+
return
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
906
|
+
try {
|
|
907
|
+
(plugin.instance as StreamCallPlugin).endCallRaw(call)
|
|
908
|
+
} catch (e: Exception) {
|
|
909
|
+
android.util.Log.e("StreamCallPlugin", "Error ending call on remote instance", e)
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
@PluginMethod
|
|
915
|
+
fun endCall(call: PluginCall) {
|
|
916
|
+
try {
|
|
917
|
+
val activeCall = streamVideoClient?.state?.activeCall
|
|
918
|
+
if (activeCall == null) {
|
|
919
|
+
android.util.Log.w("StreamCallPlugin", "Attempted to end call but no active call found")
|
|
920
|
+
call.reject("No active call to end")
|
|
921
|
+
return
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
925
|
+
try {
|
|
926
|
+
activeCall.value?.let { endCallRaw(it) }
|
|
927
|
+
call.resolve(JSObject().apply {
|
|
928
|
+
put("success", true)
|
|
929
|
+
})
|
|
930
|
+
} catch (e: Exception) {
|
|
931
|
+
android.util.Log.e("StreamCallPlugin", "Error ending call: ${e.message}")
|
|
932
|
+
call.reject("Failed to end call: ${e.message}")
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
} catch (e: Exception) {
|
|
936
|
+
call.reject("StreamVideo not initialized")
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
@PluginMethod
|
|
941
|
+
fun call(call: PluginCall) {
|
|
942
|
+
val userId = call.getString("userId")
|
|
943
|
+
if (userId == null) {
|
|
944
|
+
call.reject("Missing required parameter: userId")
|
|
945
|
+
return
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
try {
|
|
949
|
+
if (state != State.INITIALIZED) {
|
|
950
|
+
call.reject("StreamVideo not initialized")
|
|
951
|
+
return
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
val selfUserId = streamVideoClient?.userId
|
|
955
|
+
if (selfUserId == null) {
|
|
956
|
+
call.reject("No self-user id found. Are you not logged in?")
|
|
957
|
+
return
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
val callType = call.getString("type") ?: "default"
|
|
961
|
+
val shouldRing = call.getBoolean("ring") ?: true
|
|
962
|
+
val callId = java.util.UUID.randomUUID().toString()
|
|
963
|
+
|
|
964
|
+
android.util.Log.d("StreamCallPlugin", "Creating call:")
|
|
965
|
+
android.util.Log.d("StreamCallPlugin", "- Call ID: $callId")
|
|
966
|
+
android.util.Log.d("StreamCallPlugin", "- Call Type: $callType")
|
|
967
|
+
android.util.Log.d("StreamCallPlugin", "- User ID: $userId")
|
|
968
|
+
android.util.Log.d("StreamCallPlugin", "- Should Ring: $shouldRing")
|
|
969
|
+
|
|
970
|
+
// Create and join call in a coroutine
|
|
971
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
972
|
+
try {
|
|
973
|
+
// Create the call object
|
|
974
|
+
val streamCall = streamVideoClient?.call(type = callType, id = callId)
|
|
975
|
+
|
|
976
|
+
android.util.Log.d("StreamCallPlugin", "Creating call with member...")
|
|
977
|
+
// Create the call with the member
|
|
978
|
+
streamCall?.create(
|
|
979
|
+
memberIds = listOf(userId, selfUserId),
|
|
980
|
+
custom = emptyMap(),
|
|
981
|
+
ring = shouldRing
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
// streamCall?.let {
|
|
985
|
+
// this@StreamCallPlugin.streamVideoClient?.state?.setActiveCall(it)
|
|
986
|
+
// }
|
|
987
|
+
|
|
988
|
+
android.util.Log.d("StreamCallPlugin", "Setting overlay visible for outgoing call $callId")
|
|
989
|
+
// Show overlay view
|
|
990
|
+
activity?.runOnUiThread {
|
|
991
|
+
overlayView?.setContent {
|
|
992
|
+
CallOverlayView(
|
|
993
|
+
context = context,
|
|
994
|
+
streamVideo = streamVideoClient,
|
|
995
|
+
call = streamCall
|
|
996
|
+
)
|
|
997
|
+
}
|
|
998
|
+
overlayView?.isVisible = true
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Resolve the call with success
|
|
1002
|
+
call.resolve(JSObject().apply {
|
|
1003
|
+
put("success", true)
|
|
1004
|
+
})
|
|
1005
|
+
} catch (e: Exception) {
|
|
1006
|
+
android.util.Log.e("StreamCallPlugin", "Error making call: ${e.message}")
|
|
1007
|
+
call.reject("Failed to make call: ${e.message}")
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} catch (e: Exception) {
|
|
1011
|
+
call.reject("Failed to make call: ${e.message}")
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|