@capgo/capacitor-stream-call 0.0.69 → 0.0.71
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
CHANGED
|
@@ -787,7 +787,7 @@ The JSON representation for <a href="#listvalue">`ListValue`</a> is JSON array.
|
|
|
787
787
|
|
|
788
788
|
#### CallState
|
|
789
789
|
|
|
790
|
-
<code>'idle' | 'ringing' | 'joining' | 'reconnecting' | 'joined' | 'leaving' | 'left' | 'created' | 'session_started' | 'rejected' | 'missed' | 'accepted' | 'ended' | 'unknown'</code>
|
|
790
|
+
<code>'idle' | 'ringing' | 'joining' | 'reconnecting' | 'joined' | 'leaving' | 'left' | 'created' | 'session_started' | 'rejected' | 'missed' | 'accepted' | 'ended' | 'camera_enabled' | 'camera_disabled' | 'microphone_enabled' | 'microphone_disabled' | 'unknown'</code>
|
|
791
791
|
|
|
792
792
|
|
|
793
793
|
### Enums
|
|
@@ -3,6 +3,7 @@ package ee.forgr.capacitor.streamcall
|
|
|
3
3
|
import TouchInterceptWrapper
|
|
4
4
|
import android.Manifest
|
|
5
5
|
import android.app.Activity
|
|
6
|
+
import android.app.AlertDialog
|
|
6
7
|
import android.app.Application
|
|
7
8
|
import android.app.KeyguardManager
|
|
8
9
|
import android.content.BroadcastReceiver
|
|
@@ -17,12 +18,12 @@ import android.os.Build
|
|
|
17
18
|
import android.os.Bundle
|
|
18
19
|
import android.os.Handler
|
|
19
20
|
import android.os.Looper
|
|
21
|
+
import android.provider.Settings
|
|
20
22
|
import android.view.View
|
|
21
23
|
import android.view.ViewGroup
|
|
22
24
|
import android.view.WindowManager
|
|
23
25
|
import android.widget.FrameLayout
|
|
24
26
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
25
|
-
|
|
26
27
|
import androidx.compose.runtime.collectAsState
|
|
27
28
|
import androidx.compose.runtime.derivedStateOf
|
|
28
29
|
import androidx.compose.runtime.getValue
|
|
@@ -52,11 +53,9 @@ import io.getstream.android.video.generated.models.CallMissedEvent
|
|
|
52
53
|
import io.getstream.android.video.generated.models.CallRejectedEvent
|
|
53
54
|
import io.getstream.android.video.generated.models.CallRingEvent
|
|
54
55
|
import io.getstream.android.video.generated.models.CallSessionEndedEvent
|
|
55
|
-
import io.getstream.android.video.generated.models.CallSessionParticipantCountsUpdatedEvent
|
|
56
56
|
import io.getstream.android.video.generated.models.CallSessionParticipantLeftEvent
|
|
57
57
|
import io.getstream.android.video.generated.models.CallSessionStartedEvent
|
|
58
58
|
import io.getstream.android.video.generated.models.VideoEvent
|
|
59
|
-
import io.getstream.log.Priority
|
|
60
59
|
import io.getstream.video.android.compose.theme.VideoTheme
|
|
61
60
|
import io.getstream.video.android.compose.ui.components.call.activecall.CallContent
|
|
62
61
|
import io.getstream.video.android.compose.ui.components.call.renderer.FloatingParticipantVideo
|
|
@@ -69,18 +68,13 @@ import io.getstream.video.android.core.GEO
|
|
|
69
68
|
import io.getstream.video.android.core.RealtimeConnection
|
|
70
69
|
import io.getstream.video.android.core.StreamVideo
|
|
71
70
|
import io.getstream.video.android.core.StreamVideoBuilder
|
|
72
|
-
import io.getstream.video.android.core.call.CallType
|
|
73
71
|
import io.getstream.video.android.core.events.ParticipantLeftEvent
|
|
74
72
|
import io.getstream.video.android.core.internal.InternalStreamVideoApi
|
|
75
|
-
import io.getstream.video.android.core.logging.LoggingLevel
|
|
76
73
|
import io.getstream.video.android.core.notifications.NotificationConfig
|
|
77
74
|
import io.getstream.video.android.core.notifications.NotificationHandler
|
|
78
|
-
import io.getstream.video.android.core.notifications.internal.service.CallServiceConfigRegistry
|
|
79
|
-
import io.getstream.video.android.core.notifications.internal.service.DefaultCallConfigurations
|
|
80
75
|
import io.getstream.video.android.core.sounds.RingingConfig
|
|
81
76
|
import io.getstream.video.android.core.sounds.toSounds
|
|
82
77
|
import io.getstream.video.android.model.Device
|
|
83
|
-
import io.getstream.video.android.model.StreamCallId
|
|
84
78
|
import io.getstream.video.android.model.User
|
|
85
79
|
import io.getstream.video.android.model.streamCallId
|
|
86
80
|
import kotlinx.coroutines.CoroutineScope
|
|
@@ -88,6 +82,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
|
|
88
82
|
import kotlinx.coroutines.Dispatchers
|
|
89
83
|
import kotlinx.coroutines.launch
|
|
90
84
|
import kotlinx.coroutines.tasks.await
|
|
85
|
+
import androidx.core.net.toUri
|
|
91
86
|
|
|
92
87
|
// I am not a religious pearson, but at this point, I am not sure even god himself would understand this code
|
|
93
88
|
// It's a spaghetti-like, tangled, unreadable mess and frankly, I am deeply sorry for the code crimes commited in the Android impl
|
|
@@ -115,6 +110,18 @@ public class StreamCallPlugin : Plugin() {
|
|
|
115
110
|
private var callFragment: StreamCallFragment? = null
|
|
116
111
|
private var streamVideo: StreamVideo? = null
|
|
117
112
|
private var touchInterceptWrapper: TouchInterceptWrapper? = null
|
|
113
|
+
|
|
114
|
+
// Track permission request timing and attempts
|
|
115
|
+
private var permissionRequestStartTime: Long = 0
|
|
116
|
+
private var permissionAttemptCount: Int = 0
|
|
117
|
+
|
|
118
|
+
// Store pending call information for permission handling
|
|
119
|
+
private var pendingCall: PluginCall? = null
|
|
120
|
+
private var pendingCallUserIds: List<String>? = null
|
|
121
|
+
private var pendingCallType: String? = null
|
|
122
|
+
private var pendingCallShouldRing: Boolean? = null
|
|
123
|
+
private var pendingCallTeam: String? = null
|
|
124
|
+
private var pendingAcceptCall: Call? = null // Store the actual call object for acceptance
|
|
118
125
|
|
|
119
126
|
private enum class State {
|
|
120
127
|
NOT_INITIALIZED,
|
|
@@ -137,6 +144,43 @@ public class StreamCallPlugin : Plugin() {
|
|
|
137
144
|
|
|
138
145
|
override fun handleOnResume() {
|
|
139
146
|
super.handleOnResume()
|
|
147
|
+
|
|
148
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: App resumed, checking permissions and pending operations")
|
|
149
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: Have pendingCall: ${pendingCall != null}")
|
|
150
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: Have pendingCallUserIds: ${pendingCallUserIds != null}")
|
|
151
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: Have pendingAcceptCall: ${pendingAcceptCall != null}")
|
|
152
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: Permission attempt count: $permissionAttemptCount")
|
|
153
|
+
|
|
154
|
+
// Check if permissions were granted after returning from settings or permission dialog
|
|
155
|
+
if (checkPermissions()) {
|
|
156
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: Permissions are now granted")
|
|
157
|
+
// Handle any pending calls that were waiting for permissions
|
|
158
|
+
handlePermissionGranted()
|
|
159
|
+
} else if (pendingCall != null || pendingAcceptCall != null) {
|
|
160
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: Permissions still not granted, but have pending operations")
|
|
161
|
+
// If we have pending operations but permissions are still not granted,
|
|
162
|
+
// it means the permission dialog was dismissed without granting
|
|
163
|
+
// We should trigger our retry logic if we haven't exhausted attempts
|
|
164
|
+
if (permissionAttemptCount > 0) {
|
|
165
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: Permission dialog was dismissed, treating as denial (attempt: $permissionAttemptCount)")
|
|
166
|
+
val timeSinceRequest = System.currentTimeMillis() - permissionRequestStartTime
|
|
167
|
+
handlePermissionDenied(timeSinceRequest)
|
|
168
|
+
} else {
|
|
169
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: No permission attempts yet, starting permission request")
|
|
170
|
+
// If we have pending operations but no attempts yet, start the permission flow
|
|
171
|
+
if (pendingAcceptCall != null) {
|
|
172
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: Have active call waiting for permissions, requesting now")
|
|
173
|
+
permissionAttemptCount = 0
|
|
174
|
+
requestPermissions()
|
|
175
|
+
} else if (pendingCall != null && pendingCallUserIds != null) {
|
|
176
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: Have outgoing call waiting for permissions, requesting now")
|
|
177
|
+
permissionAttemptCount = 0
|
|
178
|
+
requestPermissions()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
android.util.Log.d("StreamCallPlugin", "handleOnResume: No pending operations, nothing to handle")
|
|
183
|
+
}
|
|
140
184
|
}
|
|
141
185
|
|
|
142
186
|
override fun load() {
|
|
@@ -238,7 +282,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
238
282
|
android.util.Log.d("StreamCallPlugin", " [$index] ${element.className}.${element.methodName}(${element.fileName}:${element.lineNumber})")
|
|
239
283
|
}
|
|
240
284
|
kotlinx.coroutines.GlobalScope.launch {
|
|
241
|
-
internalAcceptCall(call)
|
|
285
|
+
internalAcceptCall(call, requestPermissionsAfter = !checkPermissions())
|
|
242
286
|
}
|
|
243
287
|
bringAppToForeground()
|
|
244
288
|
} else {
|
|
@@ -634,7 +678,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
634
678
|
|
|
635
679
|
val intent = android.content.Intent(android.content.Intent.ACTION_MAIN).apply {
|
|
636
680
|
addCategory(android.content.Intent.CATEGORY_HOME)
|
|
637
|
-
flags =
|
|
681
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
638
682
|
}
|
|
639
683
|
context.startActivity(intent)
|
|
640
684
|
android.util.Log.d("StreamCallPlugin", "Moving app to background using HOME intent")
|
|
@@ -824,7 +868,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
824
868
|
updateCallStatusAndNotify(event.callCid, "left")
|
|
825
869
|
}
|
|
826
870
|
|
|
827
|
-
is ParticipantLeftEvent, is CallSessionParticipantLeftEvent
|
|
871
|
+
is ParticipantLeftEvent, is CallSessionParticipantLeftEvent -> {
|
|
828
872
|
val activeCall = streamVideoClient?.state?.activeCall?.value
|
|
829
873
|
|
|
830
874
|
val callId = when (event) {
|
|
@@ -834,9 +878,6 @@ public class StreamCallPlugin : Plugin() {
|
|
|
834
878
|
is CallSessionParticipantLeftEvent -> {
|
|
835
879
|
event.callCid
|
|
836
880
|
}
|
|
837
|
-
is CallSessionParticipantCountsUpdatedEvent -> {
|
|
838
|
-
event.callCid
|
|
839
|
-
}
|
|
840
881
|
|
|
841
882
|
else -> {
|
|
842
883
|
throw RuntimeException("Unreachable code reached when getting callId")
|
|
@@ -889,6 +930,22 @@ public class StreamCallPlugin : Plugin() {
|
|
|
889
930
|
updateCallStatusAndNotify(call.cid, "joined")
|
|
890
931
|
// Make sure activity is visible on lock screen
|
|
891
932
|
changeActivityAsVisibleOnLockScreen(this@StreamCallPlugin.activity, true)
|
|
933
|
+
|
|
934
|
+
// Listen to camera status changes
|
|
935
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
936
|
+
call.camera.isEnabled.collect { isEnabled ->
|
|
937
|
+
android.util.Log.d("StreamCallPlugin", "Camera status changed for call ${call.id}: enabled=$isEnabled")
|
|
938
|
+
updateCallStatusAndNotify(call.cid, if (isEnabled) "camera_enabled" else "camera_disabled")
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Listen to microphone status changes
|
|
943
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
944
|
+
call.microphone.isEnabled.collect { isEnabled ->
|
|
945
|
+
android.util.Log.d("StreamCallPlugin", "Microphone status changed for call ${call.id}: enabled=$isEnabled")
|
|
946
|
+
updateCallStatusAndNotify(call.cid, if (isEnabled) "microphone_enabled" else "microphone_disabled")
|
|
947
|
+
}
|
|
948
|
+
}
|
|
892
949
|
} ?: run {
|
|
893
950
|
// Notify that call has ended using our helper
|
|
894
951
|
updateCallStatusAndNotify("", "left")
|
|
@@ -959,8 +1016,20 @@ public class StreamCallPlugin : Plugin() {
|
|
|
959
1016
|
call.reject("Ringing call is null")
|
|
960
1017
|
return
|
|
961
1018
|
}
|
|
1019
|
+
|
|
1020
|
+
android.util.Log.d("StreamCallPlugin", "acceptCall: Accepting call immediately, will handle permissions after")
|
|
1021
|
+
|
|
1022
|
+
// Accept call immediately regardless of permissions - time is critical!
|
|
962
1023
|
kotlinx.coroutines.GlobalScope.launch {
|
|
963
|
-
|
|
1024
|
+
try {
|
|
1025
|
+
internalAcceptCall(streamVideoCall, requestPermissionsAfter = !checkPermissions())
|
|
1026
|
+
call.resolve(JSObject().apply {
|
|
1027
|
+
put("success", true)
|
|
1028
|
+
})
|
|
1029
|
+
} catch (e: Exception) {
|
|
1030
|
+
android.util.Log.e("StreamCallPlugin", "Error accepting call", e)
|
|
1031
|
+
call.reject("Failed to accept call: ${e.message}")
|
|
1032
|
+
}
|
|
964
1033
|
}
|
|
965
1034
|
} catch (t: Throwable) {
|
|
966
1035
|
android.util.Log.d("StreamCallPlugin", "JS -> acceptCall fail", t);
|
|
@@ -987,8 +1056,8 @@ public class StreamCallPlugin : Plugin() {
|
|
|
987
1056
|
}
|
|
988
1057
|
|
|
989
1058
|
@OptIn(DelicateCoroutinesApi::class, InternalStreamVideoApi::class)
|
|
990
|
-
internal fun internalAcceptCall(call: Call) {
|
|
991
|
-
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Entered for call: ${call.id}")
|
|
1059
|
+
internal fun internalAcceptCall(call: Call, requestPermissionsAfter: Boolean = false) {
|
|
1060
|
+
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Entered for call: ${call.id}, requestPermissionsAfter: $requestPermissionsAfter")
|
|
992
1061
|
|
|
993
1062
|
kotlinx.coroutines.GlobalScope.launch {
|
|
994
1063
|
try {
|
|
@@ -1001,26 +1070,8 @@ public class StreamCallPlugin : Plugin() {
|
|
|
1001
1070
|
}
|
|
1002
1071
|
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Incoming call view hidden for call ${call.id}")
|
|
1003
1072
|
|
|
1004
|
-
//
|
|
1005
|
-
|
|
1006
|
-
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: checkPermissions result for call ${call.id}: $permissionsGranted")
|
|
1007
|
-
if (!permissionsGranted) {
|
|
1008
|
-
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Permissions not granted for call ${call.id}. Requesting permissions.")
|
|
1009
|
-
requestPermissions()
|
|
1010
|
-
// Do not proceed with joining until permissions are granted
|
|
1011
|
-
runOnMainThread {
|
|
1012
|
-
android.widget.Toast.makeText(
|
|
1013
|
-
context,
|
|
1014
|
-
"Permissions required for call. Please grant them.",
|
|
1015
|
-
android.widget.Toast.LENGTH_LONG
|
|
1016
|
-
).show()
|
|
1017
|
-
}
|
|
1018
|
-
android.util.Log.w("StreamCallPlugin", "internalAcceptCall: Permissions not granted for call ${call.id}. Aborting accept process.")
|
|
1019
|
-
return@launch
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Permissions are granted for call ${call.id}. Proceeding to accept.")
|
|
1023
|
-
// Join the call without affecting others
|
|
1073
|
+
// Accept and join call immediately - don't wait for permissions!
|
|
1074
|
+
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Accepting call immediately for ${call.id}")
|
|
1024
1075
|
call.accept()
|
|
1025
1076
|
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: call.accept() completed for call ${call.id}")
|
|
1026
1077
|
call.join()
|
|
@@ -1039,10 +1090,15 @@ public class StreamCallPlugin : Plugin() {
|
|
|
1039
1090
|
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: WebView background set to transparent for call ${call.id}")
|
|
1040
1091
|
bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
|
|
1041
1092
|
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: WebView brought to front for call ${call.id}")
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
android.util.Log.d("StreamCallPlugin", "internalAcceptCall:
|
|
1093
|
+
|
|
1094
|
+
// Enable camera/microphone based on permissions
|
|
1095
|
+
val hasPermissions = checkPermissions()
|
|
1096
|
+
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Has permissions: $hasPermissions for call ${call.id}")
|
|
1097
|
+
|
|
1098
|
+
call.microphone?.setEnabled(hasPermissions)
|
|
1099
|
+
call.camera?.setEnabled(hasPermissions)
|
|
1100
|
+
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Microphone and camera set to $hasPermissions for call ${call.id}")
|
|
1101
|
+
|
|
1046
1102
|
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Setting CallContent with active call ${call.id}")
|
|
1047
1103
|
setOverlayContent(call)
|
|
1048
1104
|
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Content set for overlayView for call ${call.id}")
|
|
@@ -1054,6 +1110,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
1054
1110
|
parent?.removeView(overlayView)
|
|
1055
1111
|
parent?.addView(overlayView, 0) // Add at index 0 to ensure it's behind other views
|
|
1056
1112
|
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: OverlayView re-added to parent at index 0 for call ${call.id}")
|
|
1113
|
+
|
|
1057
1114
|
// Add a small delay to ensure UI refresh
|
|
1058
1115
|
mainHandler.postDelayed({
|
|
1059
1116
|
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Delayed UI check, overlay visible: ${overlayView?.isVisible} for call ${call.id}")
|
|
@@ -1075,6 +1132,19 @@ public class StreamCallPlugin : Plugin() {
|
|
|
1075
1132
|
}
|
|
1076
1133
|
}, 1000) // Increased delay to ensure all events are processed
|
|
1077
1134
|
}
|
|
1135
|
+
|
|
1136
|
+
// Request permissions after joining if needed
|
|
1137
|
+
if (requestPermissionsAfter) {
|
|
1138
|
+
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Requesting permissions after call acceptance for ${call.id}")
|
|
1139
|
+
runOnMainThread {
|
|
1140
|
+
// Store reference to the active call for enabling camera/mic later
|
|
1141
|
+
pendingAcceptCall = call
|
|
1142
|
+
android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Set pendingAcceptCall to ${call.id}, resetting attempt count")
|
|
1143
|
+
permissionAttemptCount = 0
|
|
1144
|
+
requestPermissions()
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1078
1148
|
} catch (e: Exception) {
|
|
1079
1149
|
android.util.Log.e("StreamCallPlugin", "internalAcceptCall: Error accepting call ${call.id}: ${e.message}", e)
|
|
1080
1150
|
runOnMainThread {
|
|
@@ -1100,61 +1170,481 @@ public class StreamCallPlugin : Plugin() {
|
|
|
1100
1170
|
return allGranted
|
|
1101
1171
|
}
|
|
1102
1172
|
|
|
1103
|
-
// Function to request required permissions
|
|
1104
|
-
private fun requestPermissions() {
|
|
1105
|
-
android.util.Log.d("StreamCallPlugin", "requestPermissions: Requesting RECORD_AUDIO and CAMERA permissions.")
|
|
1106
|
-
ActivityCompat.requestPermissions(
|
|
1107
|
-
activity,
|
|
1108
|
-
arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA),
|
|
1109
|
-
1001 // Request code for permission result handling
|
|
1110
|
-
)
|
|
1111
|
-
android.util.Log.d("StreamCallPlugin", "requestPermissions: ActivityCompat.requestPermissions called.")
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
1173
|
// Override to handle permission results
|
|
1115
1174
|
override fun handleRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
|
1116
1175
|
super.handleRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
1117
|
-
android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Entered. RequestCode: $requestCode")
|
|
1118
|
-
|
|
1119
|
-
|
|
1176
|
+
android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Entered. RequestCode: $requestCode, Attempt: $permissionAttemptCount")
|
|
1177
|
+
android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Expected requestCode: 9001")
|
|
1178
|
+
|
|
1179
|
+
if (requestCode == 9001) {
|
|
1180
|
+
val responseTime = System.currentTimeMillis() - permissionRequestStartTime
|
|
1181
|
+
android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Response time: ${responseTime}ms")
|
|
1182
|
+
|
|
1120
1183
|
logPermissionResults(permissions, grantResults)
|
|
1184
|
+
|
|
1121
1185
|
if (grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
|
|
1122
1186
|
android.util.Log.i("StreamCallPlugin", "handleRequestPermissionsResult: All permissions GRANTED.")
|
|
1123
|
-
//
|
|
1187
|
+
// Reset attempt count on success
|
|
1188
|
+
permissionAttemptCount = 0
|
|
1189
|
+
handlePermissionGranted()
|
|
1190
|
+
} else {
|
|
1191
|
+
android.util.Log.e("StreamCallPlugin", "handleRequestPermissionsResult: Permissions DENIED. Attempt: $permissionAttemptCount")
|
|
1192
|
+
handlePermissionDenied(responseTime)
|
|
1193
|
+
}
|
|
1194
|
+
} else {
|
|
1195
|
+
android.util.Log.w("StreamCallPlugin", "handleRequestPermissionsResult: Received unknown requestCode: $requestCode")
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
private fun logPermissionResults(permissions: Array<out String>, grantResults: IntArray) {
|
|
1200
|
+
android.util.Log.d("StreamCallPlugin", "logPermissionResults: Logging permission results:")
|
|
1201
|
+
for (i in permissions.indices) {
|
|
1202
|
+
val permission = permissions[i]
|
|
1203
|
+
val grantResult = if (grantResults.size > i) grantResults[i] else -999 // -999 for safety if arrays mismatch
|
|
1204
|
+
val resultString = if (grantResult == PackageManager.PERMISSION_GRANTED) "GRANTED" else "DENIED ($grantResult)"
|
|
1205
|
+
android.util.Log.d("StreamCallPlugin", " Permission: $permission, Result: $resultString")
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private fun handlePermissionGranted() {
|
|
1210
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Processing granted permissions")
|
|
1211
|
+
|
|
1212
|
+
// Reset attempt count since permissions are now granted
|
|
1213
|
+
permissionAttemptCount = 0
|
|
1214
|
+
|
|
1215
|
+
// Determine what type of pending operation we have
|
|
1216
|
+
val hasOutgoingCall = pendingCall != null && pendingCallUserIds != null
|
|
1217
|
+
val hasActiveCallNeedingPermissions = pendingAcceptCall != null
|
|
1218
|
+
|
|
1219
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: hasOutgoingCall=$hasOutgoingCall, hasActiveCallNeedingPermissions=$hasActiveCallNeedingPermissions")
|
|
1220
|
+
|
|
1221
|
+
when {
|
|
1222
|
+
hasOutgoingCall -> {
|
|
1223
|
+
// Outgoing call creation was waiting for permissions
|
|
1224
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Executing pending outgoing call with ${pendingCallUserIds?.size} users")
|
|
1225
|
+
executePendingCall()
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
hasActiveCallNeedingPermissions -> {
|
|
1229
|
+
// Active call needing camera/microphone enabled
|
|
1230
|
+
val callToHandle = pendingAcceptCall!!
|
|
1231
|
+
val activeCall = streamVideoClient?.state?.activeCall?.value
|
|
1232
|
+
|
|
1233
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Processing call ${callToHandle.id}")
|
|
1234
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Active call in state: ${activeCall?.id}")
|
|
1235
|
+
|
|
1236
|
+
if (activeCall != null && activeCall.id == callToHandle.id) {
|
|
1237
|
+
// Call is already active - enable camera/microphone
|
|
1238
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Enabling camera/microphone for active call ${callToHandle.id}")
|
|
1239
|
+
runOnMainThread {
|
|
1240
|
+
try {
|
|
1241
|
+
callToHandle.microphone?.setEnabled(true)
|
|
1242
|
+
callToHandle.camera?.setEnabled(true)
|
|
1243
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Camera and microphone enabled for call ${callToHandle.id}")
|
|
1244
|
+
|
|
1245
|
+
// Show success message
|
|
1246
|
+
android.widget.Toast.makeText(
|
|
1247
|
+
context,
|
|
1248
|
+
"Camera and microphone enabled",
|
|
1249
|
+
android.widget.Toast.LENGTH_SHORT
|
|
1250
|
+
).show()
|
|
1251
|
+
} catch (e: Exception) {
|
|
1252
|
+
android.util.Log.e("StreamCallPlugin", "Error enabling camera/microphone", e)
|
|
1253
|
+
}
|
|
1254
|
+
clearPendingCall()
|
|
1255
|
+
}
|
|
1256
|
+
} else if (pendingCall != null) {
|
|
1257
|
+
// Call not active yet - accept it (old flow, shouldn't happen with new flow)
|
|
1258
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Accepting pending incoming call ${callToHandle.id}")
|
|
1259
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
1260
|
+
try {
|
|
1261
|
+
internalAcceptCall(callToHandle)
|
|
1262
|
+
pendingCall?.resolve(JSObject().apply {
|
|
1263
|
+
put("success", true)
|
|
1264
|
+
})
|
|
1265
|
+
} catch (e: Exception) {
|
|
1266
|
+
android.util.Log.e("StreamCallPlugin", "Error accepting call after permission grant", e)
|
|
1267
|
+
pendingCall?.reject("Failed to accept call: ${e.message}")
|
|
1268
|
+
} finally {
|
|
1269
|
+
clearPendingCall()
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
} else {
|
|
1273
|
+
// Just enable camera/mic for the stored call even if not currently active
|
|
1274
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Enabling camera/microphone for stored call ${callToHandle.id}")
|
|
1275
|
+
runOnMainThread {
|
|
1276
|
+
try {
|
|
1277
|
+
callToHandle.microphone?.setEnabled(true)
|
|
1278
|
+
callToHandle.camera?.setEnabled(true)
|
|
1279
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Camera and microphone enabled for stored call ${callToHandle.id}")
|
|
1280
|
+
|
|
1281
|
+
android.widget.Toast.makeText(
|
|
1282
|
+
context,
|
|
1283
|
+
"Camera and microphone enabled",
|
|
1284
|
+
android.widget.Toast.LENGTH_SHORT
|
|
1285
|
+
).show()
|
|
1286
|
+
} catch (e: Exception) {
|
|
1287
|
+
android.util.Log.e("StreamCallPlugin", "Error enabling camera/microphone for stored call", e)
|
|
1288
|
+
}
|
|
1289
|
+
clearPendingCall()
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
pendingCall != null -> {
|
|
1295
|
+
// We have a pending call but unclear what type - fallback handling
|
|
1296
|
+
android.util.Log.w("StreamCallPlugin", "handlePermissionGranted: Have pendingCall but unclear operation type")
|
|
1297
|
+
android.util.Log.w("StreamCallPlugin", " - pendingCallUserIds: ${pendingCallUserIds != null}")
|
|
1298
|
+
android.util.Log.w("StreamCallPlugin", " - pendingAcceptCall: ${pendingAcceptCall != null}")
|
|
1299
|
+
|
|
1300
|
+
// Try fallback to current ringing call for acceptance
|
|
1124
1301
|
val ringingCall = streamVideoClient?.state?.ringingCall?.value
|
|
1125
|
-
android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Ringing call object: ${ringingCall?.id}")
|
|
1126
1302
|
if (ringingCall != null) {
|
|
1127
|
-
android.util.Log.d("StreamCallPlugin", "
|
|
1303
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Fallback - accepting current ringing call ${ringingCall.id}")
|
|
1128
1304
|
kotlinx.coroutines.GlobalScope.launch {
|
|
1129
|
-
|
|
1305
|
+
try {
|
|
1306
|
+
internalAcceptCall(ringingCall)
|
|
1307
|
+
pendingCall?.resolve(JSObject().apply {
|
|
1308
|
+
put("success", true)
|
|
1309
|
+
})
|
|
1310
|
+
} catch (e: Exception) {
|
|
1311
|
+
android.util.Log.e("StreamCallPlugin", "Error accepting fallback call after permission grant", e)
|
|
1312
|
+
pendingCall?.reject("Failed to accept call: ${e.message}")
|
|
1313
|
+
} finally {
|
|
1314
|
+
clearPendingCall()
|
|
1315
|
+
}
|
|
1130
1316
|
}
|
|
1131
1317
|
} else {
|
|
1132
|
-
android.util.Log.w("StreamCallPlugin", "
|
|
1318
|
+
android.util.Log.w("StreamCallPlugin", "handlePermissionGranted: No ringing call found for fallback")
|
|
1319
|
+
pendingCall?.reject("Unable to determine pending operation")
|
|
1320
|
+
clearPendingCall()
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
else -> {
|
|
1325
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: No pending operations to handle")
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
private fun handlePermissionDenied(responseTime: Long) {
|
|
1331
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionDenied: Response time: ${responseTime}ms, Attempt: $permissionAttemptCount")
|
|
1332
|
+
|
|
1333
|
+
// Check if the response was instant (< 500ms) indicating "don't ask again"
|
|
1334
|
+
val instantDenial = responseTime < 500
|
|
1335
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionDenied: Instant denial detected: $instantDenial")
|
|
1336
|
+
|
|
1337
|
+
if (instantDenial) {
|
|
1338
|
+
// If it's an instant denial (don't ask again), go straight to settings dialog
|
|
1339
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionDenied: Instant denial, showing settings dialog")
|
|
1340
|
+
showPermissionSettingsDialog()
|
|
1341
|
+
} else if (permissionAttemptCount < 2) {
|
|
1342
|
+
// Try asking again immediately if this is the first denial
|
|
1343
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionDenied: First denial (attempt $permissionAttemptCount), asking again immediately")
|
|
1344
|
+
requestPermissions() // This will increment the attempt count
|
|
1345
|
+
} else {
|
|
1346
|
+
// Second denial - show settings dialog (final ask)
|
|
1347
|
+
android.util.Log.d("StreamCallPlugin", "handlePermissionDenied: Second denial (attempt $permissionAttemptCount), showing settings dialog (final ask)")
|
|
1348
|
+
showPermissionSettingsDialog()
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
private fun executePendingCall() {
|
|
1353
|
+
val call = pendingCall
|
|
1354
|
+
val userIds = pendingCallUserIds
|
|
1355
|
+
val callType = pendingCallType
|
|
1356
|
+
val shouldRing = pendingCallShouldRing
|
|
1357
|
+
val team = pendingCallTeam
|
|
1358
|
+
|
|
1359
|
+
if (call != null && userIds != null && callType != null && shouldRing != null) {
|
|
1360
|
+
android.util.Log.d("StreamCallPlugin", "executePendingCall: Executing call with ${userIds.size} users")
|
|
1361
|
+
|
|
1362
|
+
// Clear pending call data
|
|
1363
|
+
clearPendingCall()
|
|
1364
|
+
|
|
1365
|
+
// Execute the call creation logic
|
|
1366
|
+
createAndStartCall(call, userIds, callType, shouldRing, team)
|
|
1367
|
+
} else {
|
|
1368
|
+
android.util.Log.w("StreamCallPlugin", "executePendingCall: Missing pending call data")
|
|
1369
|
+
call?.reject("Internal error: missing call parameters")
|
|
1370
|
+
clearPendingCall()
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
private fun clearPendingCall() {
|
|
1375
|
+
pendingCall = null
|
|
1376
|
+
pendingCallUserIds = null
|
|
1377
|
+
pendingCallType = null
|
|
1378
|
+
pendingCallShouldRing = null
|
|
1379
|
+
pendingCallTeam = null
|
|
1380
|
+
pendingAcceptCall = null
|
|
1381
|
+
permissionAttemptCount = 0 // Reset attempt count when clearing
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
@OptIn(DelicateCoroutinesApi::class, InternalStreamVideoApi::class)
|
|
1385
|
+
private fun createAndStartCall(call: PluginCall, userIds: List<String>, callType: String, shouldRing: Boolean, team: String?) {
|
|
1386
|
+
val selfUserId = streamVideoClient?.userId
|
|
1387
|
+
if (selfUserId == null) {
|
|
1388
|
+
call.reject("No self-user id found. Are you not logged in?")
|
|
1389
|
+
return
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
val callId = java.util.UUID.randomUUID().toString()
|
|
1393
|
+
|
|
1394
|
+
// Create and join call in a coroutine
|
|
1395
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
1396
|
+
try {
|
|
1397
|
+
// Create the call object
|
|
1398
|
+
val streamCall = streamVideoClient?.call(type = callType, id = callId)
|
|
1399
|
+
|
|
1400
|
+
// Note: We no longer start tracking here - we'll wait for CallSessionStartedEvent
|
|
1401
|
+
// instead, which contains the actual participant list
|
|
1402
|
+
|
|
1403
|
+
android.util.Log.d("StreamCallPlugin", "Creating call with members...")
|
|
1404
|
+
// Create the call with all members
|
|
1405
|
+
val createResult = streamCall?.create(
|
|
1406
|
+
memberIds = userIds + selfUserId,
|
|
1407
|
+
custom = emptyMap(),
|
|
1408
|
+
ring = shouldRing,
|
|
1409
|
+
team = team,
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1412
|
+
if (createResult?.isFailure == true) {
|
|
1413
|
+
throw (createResult.errorOrNull() ?: RuntimeException("Unknown error creating call")) as Throwable
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
android.util.Log.d("StreamCallPlugin", "Setting overlay visible for outgoing call $callId")
|
|
1417
|
+
// Show overlay view
|
|
1418
|
+
activity?.runOnUiThread {
|
|
1419
|
+
streamCall?.microphone?.setEnabled(true)
|
|
1420
|
+
streamCall?.camera?.setEnabled(true)
|
|
1421
|
+
|
|
1422
|
+
bridge?.webView?.setBackgroundColor(Color.TRANSPARENT) // Make webview transparent
|
|
1423
|
+
bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
|
|
1424
|
+
setOverlayContent(streamCall)
|
|
1425
|
+
overlayView?.isVisible = true
|
|
1426
|
+
// Ensure overlay is behind WebView by adjusting its position in the parent
|
|
1427
|
+
val parent = overlayView?.parent as? ViewGroup
|
|
1428
|
+
parent?.removeView(overlayView)
|
|
1429
|
+
parent?.addView(overlayView, 0) // Add at index 0 to ensure it's behind other views
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Resolve the call with success
|
|
1433
|
+
call.resolve(JSObject().apply {
|
|
1434
|
+
put("success", true)
|
|
1435
|
+
})
|
|
1436
|
+
} catch (e: Exception) {
|
|
1437
|
+
android.util.Log.e("StreamCallPlugin", "Error making call: ${e.message}")
|
|
1438
|
+
call.reject("Failed to make call: ${e.message}")
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Function to request required permissions
|
|
1444
|
+
private fun requestPermissions() {
|
|
1445
|
+
permissionAttemptCount++
|
|
1446
|
+
android.util.Log.d("StreamCallPlugin", "requestPermissions: Attempt #$permissionAttemptCount - Requesting RECORD_AUDIO and CAMERA permissions.")
|
|
1447
|
+
|
|
1448
|
+
// Record timing for instant denial detection
|
|
1449
|
+
permissionRequestStartTime = System.currentTimeMillis()
|
|
1450
|
+
android.util.Log.d("StreamCallPlugin", "requestPermissions: Starting permission request at $permissionRequestStartTime")
|
|
1451
|
+
|
|
1452
|
+
ActivityCompat.requestPermissions(
|
|
1453
|
+
activity,
|
|
1454
|
+
arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA),
|
|
1455
|
+
9001 // Use high request code to avoid Capacitor conflicts
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1458
|
+
android.util.Log.d("StreamCallPlugin", "requestPermissions: Permission request initiated with code 9001")
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
private fun showPermissionSettingsDialog() {
|
|
1462
|
+
activity?.runOnUiThread {
|
|
1463
|
+
val activeCall = streamVideoClient?.state?.activeCall?.value
|
|
1464
|
+
val hasActiveCall = activeCall != null && pendingAcceptCall != null && activeCall.id == pendingAcceptCall?.id
|
|
1465
|
+
|
|
1466
|
+
val builder = AlertDialog.Builder(activity)
|
|
1467
|
+
builder.setTitle("Enable Permissions")
|
|
1468
|
+
|
|
1469
|
+
if (hasActiveCall) {
|
|
1470
|
+
builder.setMessage("Your call is active but camera and microphone are disabled.\n\nWould you like to open Settings to enable video and audio?")
|
|
1471
|
+
builder.setNegativeButton("Continue without") { _, _ ->
|
|
1472
|
+
android.util.Log.d("StreamCallPlugin", "User chose to continue call without permissions")
|
|
1473
|
+
showPermissionRequiredMessage()
|
|
1474
|
+
}
|
|
1475
|
+
} else {
|
|
1476
|
+
builder.setMessage("To make video calls, this app needs Camera and Microphone permissions.\n\nWould you like to open Settings to enable them?")
|
|
1477
|
+
builder.setNegativeButton("Cancel") { _, _ ->
|
|
1478
|
+
android.util.Log.d("StreamCallPlugin", "User declined to grant permissions - final rejection")
|
|
1479
|
+
showPermissionRequiredMessage()
|
|
1133
1480
|
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
builder.setPositiveButton("Open Settings") { _, _ ->
|
|
1484
|
+
android.util.Log.d("StreamCallPlugin", "User chose to open app settings")
|
|
1485
|
+
openAppSettings()
|
|
1486
|
+
// Don't reject the call yet - let them go to settings and come back
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
builder.setCancelable(false)
|
|
1490
|
+
builder.show()
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
private fun showPermissionRequiredMessage() {
|
|
1495
|
+
activity?.runOnUiThread {
|
|
1496
|
+
val activeCall = streamVideoClient?.state?.activeCall?.value
|
|
1497
|
+
val hasActiveCall = activeCall != null && pendingAcceptCall != null && activeCall.id == pendingAcceptCall?.id
|
|
1498
|
+
|
|
1499
|
+
val builder = AlertDialog.Builder(activity)
|
|
1500
|
+
builder.setTitle("Permissions Required")
|
|
1501
|
+
|
|
1502
|
+
if (hasActiveCall) {
|
|
1503
|
+
builder.setMessage("Camera and microphone permissions are required for video calling. Your call will continue without camera/microphone.")
|
|
1134
1504
|
} else {
|
|
1135
|
-
|
|
1505
|
+
builder.setMessage("Camera/microphone permission is required for the calling functionality of this app")
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
builder.setPositiveButton("OK") { dialog, _ ->
|
|
1509
|
+
dialog.dismiss()
|
|
1510
|
+
handleFinalPermissionDenial()
|
|
1511
|
+
}
|
|
1512
|
+
builder.setCancelable(false)
|
|
1513
|
+
builder.show()
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
private fun handleFinalPermissionDenial() {
|
|
1518
|
+
android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Processing final permission denial")
|
|
1519
|
+
|
|
1520
|
+
val hasOutgoingCall = pendingCall != null && pendingCallUserIds != null
|
|
1521
|
+
val hasIncomingCall = pendingCall != null && pendingAcceptCall != null
|
|
1522
|
+
val activeCall = streamVideoClient?.state?.activeCall?.value
|
|
1523
|
+
|
|
1524
|
+
when {
|
|
1525
|
+
hasOutgoingCall -> {
|
|
1526
|
+
// Outgoing call that couldn't be created due to permissions
|
|
1527
|
+
android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Rejecting outgoing call creation")
|
|
1528
|
+
pendingCall?.reject("Permissions required for call. Please grant them.")
|
|
1529
|
+
clearPendingCall()
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
hasIncomingCall && activeCall != null && activeCall.id == pendingAcceptCall?.id -> {
|
|
1533
|
+
// Incoming call that's already active - DON'T end the call, just keep it without camera/mic
|
|
1534
|
+
android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Incoming call already active, keeping call without camera/mic")
|
|
1535
|
+
|
|
1536
|
+
// Ensure camera and microphone are disabled since no permissions
|
|
1537
|
+
try {
|
|
1538
|
+
activeCall.microphone?.setEnabled(false)
|
|
1539
|
+
activeCall.camera?.setEnabled(false)
|
|
1540
|
+
android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Disabled camera/microphone for call ${activeCall.id}")
|
|
1541
|
+
} catch (e: Exception) {
|
|
1542
|
+
android.util.Log.w("StreamCallPlugin", "handleFinalPermissionDenial: Error disabling camera/mic", e)
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
android.widget.Toast.makeText(
|
|
1546
|
+
context,
|
|
1547
|
+
"Call continues without camera/microphone",
|
|
1548
|
+
android.widget.Toast.LENGTH_LONG
|
|
1549
|
+
).show()
|
|
1550
|
+
|
|
1551
|
+
// Resolve the pending call since the call itself was successful (just no permissions)
|
|
1552
|
+
pendingCall?.resolve(JSObject().apply {
|
|
1553
|
+
put("success", true)
|
|
1554
|
+
put("message", "Call accepted without camera/microphone permissions")
|
|
1555
|
+
})
|
|
1556
|
+
clearPendingCall()
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
hasIncomingCall -> {
|
|
1560
|
+
// Incoming call that wasn't accepted yet (old flow)
|
|
1561
|
+
android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: Rejecting incoming call acceptance")
|
|
1562
|
+
pendingCall?.reject("Permissions required for call. Please grant them.")
|
|
1563
|
+
clearPendingCall()
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
else -> {
|
|
1567
|
+
android.util.Log.d("StreamCallPlugin", "handleFinalPermissionDenial: No pending operations to handle")
|
|
1568
|
+
clearPendingCall()
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
private fun openAppSettings() {
|
|
1574
|
+
try {
|
|
1575
|
+
// Try to open app-specific permission settings directly (Android 11+)
|
|
1576
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
1577
|
+
try {
|
|
1578
|
+
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
|
1579
|
+
("package:" + activity.packageName).toUri())
|
|
1580
|
+
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
|
1581
|
+
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
1582
|
+
context.startActivity(intent)
|
|
1583
|
+
android.util.Log.d("StreamCallPlugin", "Opened app details settings (Android 11+)")
|
|
1584
|
+
|
|
1585
|
+
// Show toast with specific instructions
|
|
1586
|
+
runOnMainThread {
|
|
1587
|
+
android.widget.Toast.makeText(
|
|
1588
|
+
context,
|
|
1589
|
+
"Tap 'Permissions' → Enable Camera and Microphone",
|
|
1590
|
+
android.widget.Toast.LENGTH_LONG
|
|
1591
|
+
).show()
|
|
1592
|
+
}
|
|
1593
|
+
return
|
|
1594
|
+
} catch (e: Exception) {
|
|
1595
|
+
android.util.Log.w("StreamCallPlugin", "Failed to open app details, falling back", e)
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// Fallback for older Android versions or if the above fails
|
|
1600
|
+
val intent = android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
1601
|
+
data = Uri.fromParts("package", context.packageName, null)
|
|
1602
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
1603
|
+
}
|
|
1604
|
+
context.startActivity(intent)
|
|
1605
|
+
android.util.Log.d("StreamCallPlugin", "Opened app settings via fallback")
|
|
1606
|
+
|
|
1607
|
+
// Show more specific instructions for older versions
|
|
1608
|
+
runOnMainThread {
|
|
1609
|
+
android.widget.Toast.makeText(
|
|
1610
|
+
context,
|
|
1611
|
+
"Find 'Permissions' and enable Camera + Microphone",
|
|
1612
|
+
android.widget.Toast.LENGTH_LONG
|
|
1613
|
+
).show()
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
} catch (e: Exception) {
|
|
1617
|
+
android.util.Log.e("StreamCallPlugin", "Error opening app settings", e)
|
|
1618
|
+
|
|
1619
|
+
// Final fallback - open general settings
|
|
1620
|
+
try {
|
|
1621
|
+
val intent = android.content.Intent(android.provider.Settings.ACTION_SETTINGS).apply {
|
|
1622
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
1623
|
+
}
|
|
1624
|
+
context.startActivity(intent)
|
|
1625
|
+
android.util.Log.d("StreamCallPlugin", "Opened general settings as final fallback")
|
|
1626
|
+
|
|
1136
1627
|
runOnMainThread {
|
|
1137
1628
|
android.widget.Toast.makeText(
|
|
1138
1629
|
context,
|
|
1139
|
-
"
|
|
1630
|
+
"Go to Apps → ${context.applicationInfo.loadLabel(context.packageManager)} → Permissions",
|
|
1631
|
+
android.widget.Toast.LENGTH_LONG
|
|
1632
|
+
).show()
|
|
1633
|
+
}
|
|
1634
|
+
} catch (finalException: Exception) {
|
|
1635
|
+
android.util.Log.e("StreamCallPlugin", "All settings intents failed", finalException)
|
|
1636
|
+
runOnMainThread {
|
|
1637
|
+
android.widget.Toast.makeText(
|
|
1638
|
+
context,
|
|
1639
|
+
"Please manually enable Camera and Microphone permissions",
|
|
1140
1640
|
android.widget.Toast.LENGTH_LONG
|
|
1141
1641
|
).show()
|
|
1142
1642
|
}
|
|
1143
1643
|
}
|
|
1144
|
-
} else {
|
|
1145
|
-
android.util.Log.w("StreamCallPlugin", "handleRequestPermissionsResult: Received unknown requestCode: $requestCode")
|
|
1146
1644
|
}
|
|
1147
1645
|
}
|
|
1148
1646
|
|
|
1149
|
-
|
|
1150
|
-
android.util.Log.d("StreamCallPlugin", "logPermissionResults: Logging permission results:")
|
|
1151
|
-
for (i in permissions.indices) {
|
|
1152
|
-
val permission = permissions[i]
|
|
1153
|
-
val grantResult = if (grantResults.size > i) grantResults[i] else -999 // -999 for safety if arrays mismatch
|
|
1154
|
-
val resultString = if (grantResult == PackageManager.PERMISSION_GRANTED) "GRANTED" else "DENIED ($grantResult)"
|
|
1155
|
-
android.util.Log.d("StreamCallPlugin", " Permission: $permission, Result: $resultString")
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1647
|
+
|
|
1158
1648
|
|
|
1159
1649
|
@OptIn(DelicateCoroutinesApi::class)
|
|
1160
1650
|
@PluginMethod
|
|
@@ -1459,58 +1949,21 @@ public class StreamCallPlugin : Plugin() {
|
|
|
1459
1949
|
|
|
1460
1950
|
// Check permissions before creating the call
|
|
1461
1951
|
if (!checkPermissions()) {
|
|
1952
|
+
android.util.Log.d("StreamCallPlugin", "Permissions not granted, storing call parameters and requesting permissions")
|
|
1953
|
+
// Store call parameters for later execution
|
|
1954
|
+
pendingCall = call
|
|
1955
|
+
pendingCallUserIds = userIds
|
|
1956
|
+
pendingCallType = callType
|
|
1957
|
+
pendingCallShouldRing = shouldRing
|
|
1958
|
+
pendingCallTeam = team
|
|
1959
|
+
// Reset attempt count for new permission flow
|
|
1960
|
+
permissionAttemptCount = 0
|
|
1462
1961
|
requestPermissions()
|
|
1463
|
-
|
|
1464
|
-
return
|
|
1962
|
+
return // Don't reject immediately, wait for permission result
|
|
1465
1963
|
}
|
|
1466
1964
|
|
|
1467
|
-
//
|
|
1468
|
-
|
|
1469
|
-
try {
|
|
1470
|
-
// Create the call object
|
|
1471
|
-
val streamCall = streamVideoClient?.call(type = callType, id = callId)
|
|
1472
|
-
|
|
1473
|
-
// Note: We no longer start tracking here - we'll wait for CallSessionStartedEvent
|
|
1474
|
-
// instead, which contains the actual participant list
|
|
1475
|
-
|
|
1476
|
-
android.util.Log.d("StreamCallPlugin", "Creating call with members...")
|
|
1477
|
-
// Create the call with all members
|
|
1478
|
-
val createResult = streamCall?.create(
|
|
1479
|
-
memberIds = userIds + selfUserId,
|
|
1480
|
-
custom = emptyMap(),
|
|
1481
|
-
ring = shouldRing,
|
|
1482
|
-
team = team,
|
|
1483
|
-
)
|
|
1484
|
-
|
|
1485
|
-
if (createResult?.isFailure == true) {
|
|
1486
|
-
throw (createResult.errorOrNull() ?: RuntimeException("Unknown error creating call")) as Throwable
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
android.util.Log.d("StreamCallPlugin", "Setting overlay visible for outgoing call $callId")
|
|
1490
|
-
// Show overlay view
|
|
1491
|
-
activity?.runOnUiThread {
|
|
1492
|
-
streamCall?.microphone?.setEnabled(true)
|
|
1493
|
-
streamCall?.camera?.setEnabled(true)
|
|
1494
|
-
|
|
1495
|
-
bridge?.webView?.setBackgroundColor(Color.TRANSPARENT) // Make webview transparent
|
|
1496
|
-
bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
|
|
1497
|
-
setOverlayContent(streamCall)
|
|
1498
|
-
overlayView?.isVisible = true
|
|
1499
|
-
// Ensure overlay is behind WebView by adjusting its position in the parent
|
|
1500
|
-
val parent = overlayView?.parent as? ViewGroup
|
|
1501
|
-
parent?.removeView(overlayView)
|
|
1502
|
-
parent?.addView(overlayView, 0) // Add at index 0 to ensure it's behind other views
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
// Resolve the call with success
|
|
1506
|
-
call.resolve(JSObject().apply {
|
|
1507
|
-
put("success", true)
|
|
1508
|
-
})
|
|
1509
|
-
} catch (e: Exception) {
|
|
1510
|
-
android.util.Log.e("StreamCallPlugin", "Error making call: ${e.message}")
|
|
1511
|
-
call.reject("Failed to make call: ${e.message}")
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1965
|
+
// Execute call creation immediately if permissions are granted
|
|
1966
|
+
createAndStartCall(call, userIds, callType, shouldRing, team)
|
|
1514
1967
|
} catch (e: Exception) {
|
|
1515
1968
|
call.reject("Failed to make call: ${e.message}")
|
|
1516
1969
|
}
|
|
@@ -1726,7 +2179,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
1726
2179
|
call.reject("No active call")
|
|
1727
2180
|
}
|
|
1728
2181
|
}
|
|
1729
|
-
|
|
2182
|
+
|
|
1730
2183
|
// Helper method to update call status and notify listeners
|
|
1731
2184
|
private fun updateCallStatusAndNotify(callId: String, state: String, userId: String? = null, reason: String? = null, members: List<Map<String, Any>>? = null, caller: Map<String, Any>? = null) {
|
|
1732
2185
|
android.util.Log.d("StreamCallPlugin", "updateCallStatusAndNotify called: callId=$callId, state=$state, userId=$userId, reason=$reason")
|
|
@@ -1824,7 +2277,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
1824
2277
|
val call = streamVideoClient?.call(id = cid.id, type = cid.type)
|
|
1825
2278
|
if (call != null) {
|
|
1826
2279
|
kotlinx.coroutines.GlobalScope.launch {
|
|
1827
|
-
internalAcceptCall(call)
|
|
2280
|
+
internalAcceptCall(call, requestPermissionsAfter = !checkPermissions())
|
|
1828
2281
|
}
|
|
1829
2282
|
bringAppToForeground()
|
|
1830
2283
|
} else {
|
package/dist/docs.json
CHANGED
|
@@ -1565,6 +1565,22 @@
|
|
|
1565
1565
|
"text": "'ended'",
|
|
1566
1566
|
"complexTypes": []
|
|
1567
1567
|
},
|
|
1568
|
+
{
|
|
1569
|
+
"text": "'camera_enabled'",
|
|
1570
|
+
"complexTypes": []
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
"text": "'camera_disabled'",
|
|
1574
|
+
"complexTypes": []
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
"text": "'microphone_enabled'",
|
|
1578
|
+
"complexTypes": []
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
"text": "'microphone_disabled'",
|
|
1582
|
+
"complexTypes": []
|
|
1583
|
+
},
|
|
1568
1584
|
{
|
|
1569
1585
|
"text": "'unknown'",
|
|
1570
1586
|
"complexTypes": []
|
|
@@ -31,7 +31,7 @@ export interface PushNotificationsConfig {
|
|
|
31
31
|
* @typedef CallState
|
|
32
32
|
* @description Represents all possible call states from API and UI
|
|
33
33
|
*/
|
|
34
|
-
export type CallState = 'idle' | 'ringing' | 'joining' | 'reconnecting' | 'joined' | 'leaving' | 'left' | 'created' | 'session_started' | 'rejected' | 'missed' | 'accepted' | 'ended' | 'unknown';
|
|
34
|
+
export type CallState = 'idle' | 'ringing' | 'joining' | 'reconnecting' | 'joined' | 'leaving' | 'left' | 'created' | 'session_started' | 'rejected' | 'missed' | 'accepted' | 'ended' | 'camera_enabled' | 'camera_disabled' | 'microphone_enabled' | 'microphone_disabled' | 'unknown';
|
|
35
35
|
/**
|
|
36
36
|
* @typedef CallType
|
|
37
37
|
* @description Represents the pre-defined types of a call.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * @interface LoginOptions\n * @description Configuration options for logging into the Stream Video service\n * @property {string} token - Stream Video API token for authentication\n * @property {string} userId - Unique identifier for the current user\n * @property {string} name - Display name for the current user\n * @property {string} [imageURL] - Avatar URL for the current user\n * @property {string} apiKey - Stream Video API key for your application\n * @property {string} [magicDivId] - DOM element ID where video will be rendered\n */\nexport interface LoginOptions {\n /** Stream Video API token */\n token: string;\n /** User ID for the current user */\n userId: string;\n /** Display name for the current user */\n name: string;\n /** Optional avatar URL for the current user */\n imageURL?: string;\n /** Stream Video API key */\n apiKey: string;\n /** ID of the HTML element where the video will be rendered */\n magicDivId?: string;\n pushNotificationsConfig?: PushNotificationsConfig;\n}\n\nexport interface PushNotificationsConfig {\n pushProviderName: string;\n voipProviderName: string;\n}\n\n/**\n * @typedef CallState\n * @description Represents all possible call states from API and UI\n */\nexport type CallState =\n // User-facing states\n | 'idle'\n | 'ringing'\n | 'joining'\n | 'reconnecting'\n | 'joined'\n | 'leaving'\n | 'left'\n // Event-specific states\n | 'created'\n | 'session_started'\n | 'rejected'\n | 'missed'\n | 'accepted'\n | 'ended'\n | 'unknown';\n\n/**\n * @typedef CallType\n * @description Represents the pre-defined types of a call.\n * - `default`: Simple 1-1 or group video calling with sensible defaults. Video/audio enabled, backstage disabled. Admins/hosts have elevated permissions.\n * - `audio_room`: For audio-only spaces (like Clubhouse). Backstage enabled (requires `goLive`), pre-configured permissions for requesting to speak.\n * - `livestream`: For one-to-many streaming. Backstage enabled (requires `goLive`), access granted to all authenticated users.\n * - `development`: For testing ONLY. All permissions enabled, backstage disabled. **Not recommended for production.**\n */\nexport type CallType = 'default' | 'audio_room' | 'livestream' | 'development';\n\n/**\n * @interface CallMember\n * @description Information about a call member/participant\n * @property {string} userId - User ID of the member\n * @property {string} [name] - Display name of the user\n * @property {string} [imageURL] - Profile image URL of the user\n * @property {string} [role] - Role of the user in the call\n */\nexport interface CallMember {\n /** User ID of the member */\n userId: string;\n /** Display name of the user */\n name?: string;\n /** Profile image URL of the user */\n imageURL?: string;\n /** Role of the user in the call */\n role?: string;\n}\n\n/**\n * @interface CallEvent\n * @description Event emitted when call state changes\n * @property {string} callId - Unique identifier of the call\n * @property {CallState} state - Current state of the call\n * @property {string} [userId] - User ID of the participant who triggered the event\n * @property {string} [reason] - Reason for the call state change\n * @property {CallMember} [caller] - Information about the caller (for incoming calls)\n * @property {CallMember[]} [members] - List of call members\n */\nexport interface CallEvent {\n /** ID of the call */\n callId: string;\n /** Current state of the call */\n state: CallState;\n /** User ID of the participant in the call who triggered the event */\n userId?: string;\n /** Reason for the call state change, if applicable */\n reason?: string;\n /** Information about the caller (for incoming calls) */\n caller?: CallMember;\n /** List of call members */\n members?: CallMember[];\n}\n\nexport interface CameraEnabledResponse {\n enabled: boolean;\n}\n\n/**\n * @interface SuccessResponse\n * @description Standard response indicating operation success/failure\n * @property {boolean} success - Whether the operation succeeded\n */\nexport interface SuccessResponse {\n /** Whether the operation was successful */\n success: boolean;\n}\n\n/**\n * @interface CallOptions\n * @description Options for initiating a video call\n * @property {string[]} userIds - IDs of the users to call\n * @property {CallType} [type=default] - Type of call\n * @property {boolean} [ring=true] - Whether to send ring notification\n * @property {string} [team] - Team name to call\n */\nexport interface CallOptions {\n /** User ID of the person to call */\n userIds: string[];\n /** Type of call, defaults to 'default' */\n type?: CallType;\n /** Whether to ring the other user, defaults to true */\n ring?: boolean;\n /** Team name to call */\n team?: string;\n /** Whether to start the call with video enabled, defaults to false */\n video?: boolean;\n}\n\n/**\n * @interface StreamCallPlugin\n * @description Capacitor plugin for Stream Video calling functionality\n */\nexport interface StreamCallPlugin {\n /**\n * Login to Stream Video service\n * @param {LoginOptions} options - Login configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.login({\n * token: 'your-token',\n * userId: 'user-123',\n * name: 'John Doe',\n * apiKey: 'your-api-key'\n * });\n */\n login(options: LoginOptions): Promise<SuccessResponse>;\n\n /**\n * Logout from Stream Video service\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.logout();\n */\n logout(): Promise<SuccessResponse>;\n\n /**\n * Initiate a call to another user\n * @param {CallOptions} options - Call configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.call({\n * userId: 'user-456',\n * type: 'video',\n * ring: true\n * });\n */\n call(options: CallOptions): Promise<SuccessResponse>;\n\n /**\n * End the current call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.endCall();\n */\n endCall(): Promise<SuccessResponse>;\n\n /**\n * Enable or disable microphone\n * @param {{ enabled: boolean }} options - Microphone state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setMicrophoneEnabled({ enabled: false });\n */\n setMicrophoneEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Enable or disable camera\n * @param {{ enabled: boolean }} options - Camera state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setCameraEnabled({ enabled: false });\n */\n setCameraEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Add listener for call events\n * @param {'callEvent'} eventName - Name of the event to listen for\n * @param {(event: CallEvent) => void} listenerFunc - Callback function\n * @returns {Promise<{ remove: () => Promise<void> }>} Function to remove listener\n * @example\n * const listener = await StreamCall.addListener('callEvent', (event) => {\n * console.log(`Call ${event.callId} is now ${event.state}`);\n * });\n */\n addListener(\n eventName: 'callEvent',\n listenerFunc: (event: CallEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Listen for lock-screen incoming call (Android only).\n * Fired when the app is shown by full-screen intent before user interaction.\n */\n addListener(\n eventName: 'incomingCall',\n listenerFunc: (event: IncomingCallPayload) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Remove all event listeners\n * @returns {Promise<void>}\n * @example\n * await StreamCall.removeAllListeners();\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Accept an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.acceptCall();\n */\n acceptCall(): Promise<SuccessResponse>;\n\n /**\n * Reject an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.rejectCall();\n */\n rejectCall(): Promise<SuccessResponse>;\n\n /**\n * Check if camera is enabled\n * @returns {Promise<CameraEnabledResponse>} Camera enabled status\n * @example\n * const isCameraEnabled = await StreamCall.isCameraEnabled();\n * console.log(isCameraEnabled);\n */\n isCameraEnabled(): Promise<CameraEnabledResponse>;\n\n /**\n * Get the current call status\n * @returns {Promise<CallEvent>} Current call status as a CallEvent\n * @example\n * const callStatus = await StreamCall.getCallStatus();\n * console.log(callStatus);\n */\n getCallStatus(): Promise<CallEvent>;\n\n /**\n * Set speakerphone on\n * @param {{ name: string }} options - Speakerphone name\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setSpeaker({ name: 'speaker' });\n */\n setSpeaker(options: { name: string }): Promise<SuccessResponse>;\n\n /**\n * Switch camera\n * @param {{ camera: 'front' | 'back' }} options - Camera to switch to\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.switchCamera({ camera: 'back' });\n */\n switchCamera(options: { camera: 'front' | 'back' }): Promise<SuccessResponse>;\n\n /**\n * Get detailed information about an active call including caller details\n * @param options - Options containing the call ID\n */\n getCallInfo(options: { callId: string }): Promise<CallEvent>;\n}\n\n/**\n * @interface IncomingCallPayload\n * @description Payload delivered with \"incomingCall\" event (Android lock-screen).\n * @property {string} cid - Call CID (type:id)\n * @property {string} type - Always \"incoming\" for this event\n * @property {CallMember} [caller] - Information about the caller\n */\nexport interface IncomingCallPayload {\n /** Full call CID (e.g. default:123) */\n cid: string;\n /** Event type (currently always \"incoming\") */\n type: 'incoming';\n /** Information about the caller */\n caller?: CallMember;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * @interface LoginOptions\n * @description Configuration options for logging into the Stream Video service\n * @property {string} token - Stream Video API token for authentication\n * @property {string} userId - Unique identifier for the current user\n * @property {string} name - Display name for the current user\n * @property {string} [imageURL] - Avatar URL for the current user\n * @property {string} apiKey - Stream Video API key for your application\n * @property {string} [magicDivId] - DOM element ID where video will be rendered\n */\nexport interface LoginOptions {\n /** Stream Video API token */\n token: string;\n /** User ID for the current user */\n userId: string;\n /** Display name for the current user */\n name: string;\n /** Optional avatar URL for the current user */\n imageURL?: string;\n /** Stream Video API key */\n apiKey: string;\n /** ID of the HTML element where the video will be rendered */\n magicDivId?: string;\n pushNotificationsConfig?: PushNotificationsConfig;\n}\n\nexport interface PushNotificationsConfig {\n pushProviderName: string;\n voipProviderName: string;\n}\n\n/**\n * @typedef CallState\n * @description Represents all possible call states from API and UI\n */\nexport type CallState =\n // User-facing states\n | 'idle'\n | 'ringing'\n | 'joining'\n | 'reconnecting'\n | 'joined'\n | 'leaving'\n | 'left'\n // Event-specific states\n | 'created'\n | 'session_started'\n | 'rejected'\n | 'missed'\n | 'accepted'\n | 'ended'\n | 'camera_enabled'\n | 'camera_disabled'\n | 'microphone_enabled'\n | 'microphone_disabled'\n | 'unknown';\n\n/**\n * @typedef CallType\n * @description Represents the pre-defined types of a call.\n * - `default`: Simple 1-1 or group video calling with sensible defaults. Video/audio enabled, backstage disabled. Admins/hosts have elevated permissions.\n * - `audio_room`: For audio-only spaces (like Clubhouse). Backstage enabled (requires `goLive`), pre-configured permissions for requesting to speak.\n * - `livestream`: For one-to-many streaming. Backstage enabled (requires `goLive`), access granted to all authenticated users.\n * - `development`: For testing ONLY. All permissions enabled, backstage disabled. **Not recommended for production.**\n */\nexport type CallType = 'default' | 'audio_room' | 'livestream' | 'development';\n\n/**\n * @interface CallMember\n * @description Information about a call member/participant\n * @property {string} userId - User ID of the member\n * @property {string} [name] - Display name of the user\n * @property {string} [imageURL] - Profile image URL of the user\n * @property {string} [role] - Role of the user in the call\n */\nexport interface CallMember {\n /** User ID of the member */\n userId: string;\n /** Display name of the user */\n name?: string;\n /** Profile image URL of the user */\n imageURL?: string;\n /** Role of the user in the call */\n role?: string;\n}\n\n/**\n * @interface CallEvent\n * @description Event emitted when call state changes\n * @property {string} callId - Unique identifier of the call\n * @property {CallState} state - Current state of the call\n * @property {string} [userId] - User ID of the participant who triggered the event\n * @property {string} [reason] - Reason for the call state change\n * @property {CallMember} [caller] - Information about the caller (for incoming calls)\n * @property {CallMember[]} [members] - List of call members\n */\nexport interface CallEvent {\n /** ID of the call */\n callId: string;\n /** Current state of the call */\n state: CallState;\n /** User ID of the participant in the call who triggered the event */\n userId?: string;\n /** Reason for the call state change, if applicable */\n reason?: string;\n /** Information about the caller (for incoming calls) */\n caller?: CallMember;\n /** List of call members */\n members?: CallMember[];\n}\n\nexport interface CameraEnabledResponse {\n enabled: boolean;\n}\n\n/**\n * @interface SuccessResponse\n * @description Standard response indicating operation success/failure\n * @property {boolean} success - Whether the operation succeeded\n */\nexport interface SuccessResponse {\n /** Whether the operation was successful */\n success: boolean;\n}\n\n/**\n * @interface CallOptions\n * @description Options for initiating a video call\n * @property {string[]} userIds - IDs of the users to call\n * @property {CallType} [type=default] - Type of call\n * @property {boolean} [ring=true] - Whether to send ring notification\n * @property {string} [team] - Team name to call\n */\nexport interface CallOptions {\n /** User ID of the person to call */\n userIds: string[];\n /** Type of call, defaults to 'default' */\n type?: CallType;\n /** Whether to ring the other user, defaults to true */\n ring?: boolean;\n /** Team name to call */\n team?: string;\n /** Whether to start the call with video enabled, defaults to false */\n video?: boolean;\n}\n\n/**\n * @interface StreamCallPlugin\n * @description Capacitor plugin for Stream Video calling functionality\n */\nexport interface StreamCallPlugin {\n /**\n * Login to Stream Video service\n * @param {LoginOptions} options - Login configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.login({\n * token: 'your-token',\n * userId: 'user-123',\n * name: 'John Doe',\n * apiKey: 'your-api-key'\n * });\n */\n login(options: LoginOptions): Promise<SuccessResponse>;\n\n /**\n * Logout from Stream Video service\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.logout();\n */\n logout(): Promise<SuccessResponse>;\n\n /**\n * Initiate a call to another user\n * @param {CallOptions} options - Call configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.call({\n * userId: 'user-456',\n * type: 'video',\n * ring: true\n * });\n */\n call(options: CallOptions): Promise<SuccessResponse>;\n\n /**\n * End the current call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.endCall();\n */\n endCall(): Promise<SuccessResponse>;\n\n /**\n * Enable or disable microphone\n * @param {{ enabled: boolean }} options - Microphone state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setMicrophoneEnabled({ enabled: false });\n */\n setMicrophoneEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Enable or disable camera\n * @param {{ enabled: boolean }} options - Camera state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setCameraEnabled({ enabled: false });\n */\n setCameraEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Add listener for call events\n * @param {'callEvent'} eventName - Name of the event to listen for\n * @param {(event: CallEvent) => void} listenerFunc - Callback function\n * @returns {Promise<{ remove: () => Promise<void> }>} Function to remove listener\n * @example\n * const listener = await StreamCall.addListener('callEvent', (event) => {\n * console.log(`Call ${event.callId} is now ${event.state}`);\n * });\n */\n addListener(\n eventName: 'callEvent',\n listenerFunc: (event: CallEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Listen for lock-screen incoming call (Android only).\n * Fired when the app is shown by full-screen intent before user interaction.\n */\n addListener(\n eventName: 'incomingCall',\n listenerFunc: (event: IncomingCallPayload) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Remove all event listeners\n * @returns {Promise<void>}\n * @example\n * await StreamCall.removeAllListeners();\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Accept an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.acceptCall();\n */\n acceptCall(): Promise<SuccessResponse>;\n\n /**\n * Reject an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.rejectCall();\n */\n rejectCall(): Promise<SuccessResponse>;\n\n /**\n * Check if camera is enabled\n * @returns {Promise<CameraEnabledResponse>} Camera enabled status\n * @example\n * const isCameraEnabled = await StreamCall.isCameraEnabled();\n * console.log(isCameraEnabled);\n */\n isCameraEnabled(): Promise<CameraEnabledResponse>;\n\n /**\n * Get the current call status\n * @returns {Promise<CallEvent>} Current call status as a CallEvent\n * @example\n * const callStatus = await StreamCall.getCallStatus();\n * console.log(callStatus);\n */\n getCallStatus(): Promise<CallEvent>;\n\n /**\n * Set speakerphone on\n * @param {{ name: string }} options - Speakerphone name\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setSpeaker({ name: 'speaker' });\n */\n setSpeaker(options: { name: string }): Promise<SuccessResponse>;\n\n /**\n * Switch camera\n * @param {{ camera: 'front' | 'back' }} options - Camera to switch to\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.switchCamera({ camera: 'back' });\n */\n switchCamera(options: { camera: 'front' | 'back' }): Promise<SuccessResponse>;\n\n /**\n * Get detailed information about an active call including caller details\n * @param options - Options containing the call ID\n */\n getCallInfo(options: { callId: string }): Promise<CallEvent>;\n}\n\n/**\n * @interface IncomingCallPayload\n * @description Payload delivered with \"incomingCall\" event (Android lock-screen).\n * @property {string} cid - Call CID (type:id)\n * @property {string} type - Always \"incoming\" for this event\n * @property {CallMember} [caller] - Information about the caller\n */\nexport interface IncomingCallPayload {\n /** Full call CID (e.g. default:123) */\n cid: string;\n /** Event type (currently always \"incoming\") */\n type: 'incoming';\n /** Information about the caller */\n caller?: CallMember;\n}\n"]}
|
package/package.json
CHANGED