@capgo/capacitor-stream-call 0.0.70 → 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
@@ -55,7 +56,6 @@ import io.getstream.android.video.generated.models.CallSessionEndedEvent
55
56
  import io.getstream.android.video.generated.models.CallSessionParticipantLeftEvent
56
57
  import io.getstream.android.video.generated.models.CallSessionStartedEvent
57
58
  import io.getstream.android.video.generated.models.VideoEvent
58
- import io.getstream.log.Priority
59
59
  import io.getstream.video.android.compose.theme.VideoTheme
60
60
  import io.getstream.video.android.compose.ui.components.call.activecall.CallContent
61
61
  import io.getstream.video.android.compose.ui.components.call.renderer.FloatingParticipantVideo
@@ -68,18 +68,13 @@ import io.getstream.video.android.core.GEO
68
68
  import io.getstream.video.android.core.RealtimeConnection
69
69
  import io.getstream.video.android.core.StreamVideo
70
70
  import io.getstream.video.android.core.StreamVideoBuilder
71
- import io.getstream.video.android.core.call.CallType
72
71
  import io.getstream.video.android.core.events.ParticipantLeftEvent
73
72
  import io.getstream.video.android.core.internal.InternalStreamVideoApi
74
- import io.getstream.video.android.core.logging.LoggingLevel
75
73
  import io.getstream.video.android.core.notifications.NotificationConfig
76
74
  import io.getstream.video.android.core.notifications.NotificationHandler
77
- import io.getstream.video.android.core.notifications.internal.service.CallServiceConfigRegistry
78
- import io.getstream.video.android.core.notifications.internal.service.DefaultCallConfigurations
79
75
  import io.getstream.video.android.core.sounds.RingingConfig
80
76
  import io.getstream.video.android.core.sounds.toSounds
81
77
  import io.getstream.video.android.model.Device
82
- import io.getstream.video.android.model.StreamCallId
83
78
  import io.getstream.video.android.model.User
84
79
  import io.getstream.video.android.model.streamCallId
85
80
  import kotlinx.coroutines.CoroutineScope
@@ -87,6 +82,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
87
82
  import kotlinx.coroutines.Dispatchers
88
83
  import kotlinx.coroutines.launch
89
84
  import kotlinx.coroutines.tasks.await
85
+ import androidx.core.net.toUri
90
86
 
91
87
  // I am not a religious pearson, but at this point, I am not sure even god himself would understand this code
92
88
  // It's a spaghetti-like, tangled, unreadable mess and frankly, I am deeply sorry for the code crimes commited in the Android impl
@@ -114,6 +110,18 @@ public class StreamCallPlugin : Plugin() {
114
110
  private var callFragment: StreamCallFragment? = null
115
111
  private var streamVideo: StreamVideo? = null
116
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
117
125
 
118
126
  private enum class State {
119
127
  NOT_INITIALIZED,
@@ -136,6 +144,43 @@ public class StreamCallPlugin : Plugin() {
136
144
 
137
145
  override fun handleOnResume() {
138
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
+ }
139
184
  }
140
185
 
141
186
  override fun load() {
@@ -237,7 +282,7 @@ public class StreamCallPlugin : Plugin() {
237
282
  android.util.Log.d("StreamCallPlugin", " [$index] ${element.className}.${element.methodName}(${element.fileName}:${element.lineNumber})")
238
283
  }
239
284
  kotlinx.coroutines.GlobalScope.launch {
240
- internalAcceptCall(call)
285
+ internalAcceptCall(call, requestPermissionsAfter = !checkPermissions())
241
286
  }
242
287
  bringAppToForeground()
243
288
  } else {
@@ -633,7 +678,7 @@ public class StreamCallPlugin : Plugin() {
633
678
 
634
679
  val intent = android.content.Intent(android.content.Intent.ACTION_MAIN).apply {
635
680
  addCategory(android.content.Intent.CATEGORY_HOME)
636
- flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK
681
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
637
682
  }
638
683
  context.startActivity(intent)
639
684
  android.util.Log.d("StreamCallPlugin", "Moving app to background using HOME intent")
@@ -885,6 +930,22 @@ public class StreamCallPlugin : Plugin() {
885
930
  updateCallStatusAndNotify(call.cid, "joined")
886
931
  // Make sure activity is visible on lock screen
887
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
+ }
888
949
  } ?: run {
889
950
  // Notify that call has ended using our helper
890
951
  updateCallStatusAndNotify("", "left")
@@ -955,8 +1016,20 @@ public class StreamCallPlugin : Plugin() {
955
1016
  call.reject("Ringing call is null")
956
1017
  return
957
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!
958
1023
  kotlinx.coroutines.GlobalScope.launch {
959
- internalAcceptCall(streamVideoCall)
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
+ }
960
1033
  }
961
1034
  } catch (t: Throwable) {
962
1035
  android.util.Log.d("StreamCallPlugin", "JS -> acceptCall fail", t);
@@ -983,8 +1056,8 @@ public class StreamCallPlugin : Plugin() {
983
1056
  }
984
1057
 
985
1058
  @OptIn(DelicateCoroutinesApi::class, InternalStreamVideoApi::class)
986
- internal fun internalAcceptCall(call: Call) {
987
- 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")
988
1061
 
989
1062
  kotlinx.coroutines.GlobalScope.launch {
990
1063
  try {
@@ -997,26 +1070,8 @@ public class StreamCallPlugin : Plugin() {
997
1070
  }
998
1071
  android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Incoming call view hidden for call ${call.id}")
999
1072
 
1000
- // Check and request permissions before joining the call
1001
- val permissionsGranted = checkPermissions()
1002
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: checkPermissions result for call ${call.id}: $permissionsGranted")
1003
- if (!permissionsGranted) {
1004
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Permissions not granted for call ${call.id}. Requesting permissions.")
1005
- requestPermissions()
1006
- // Do not proceed with joining until permissions are granted
1007
- runOnMainThread {
1008
- android.widget.Toast.makeText(
1009
- context,
1010
- "Permissions required for call. Please grant them.",
1011
- android.widget.Toast.LENGTH_LONG
1012
- ).show()
1013
- }
1014
- android.util.Log.w("StreamCallPlugin", "internalAcceptCall: Permissions not granted for call ${call.id}. Aborting accept process.")
1015
- return@launch
1016
- }
1017
-
1018
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Permissions are granted for call ${call.id}. Proceeding to accept.")
1019
- // 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}")
1020
1075
  call.accept()
1021
1076
  android.util.Log.d("StreamCallPlugin", "internalAcceptCall: call.accept() completed for call ${call.id}")
1022
1077
  call.join()
@@ -1035,10 +1090,15 @@ public class StreamCallPlugin : Plugin() {
1035
1090
  android.util.Log.d("StreamCallPlugin", "internalAcceptCall: WebView background set to transparent for call ${call.id}")
1036
1091
  bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
1037
1092
  android.util.Log.d("StreamCallPlugin", "internalAcceptCall: WebView brought to front for call ${call.id}")
1038
- // Reusing the initialization logic from call method
1039
- call.microphone?.setEnabled(true)
1040
- call.camera?.setEnabled(true)
1041
- android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Microphone and camera enabled for call ${call.id}")
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
+
1042
1102
  android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Setting CallContent with active call ${call.id}")
1043
1103
  setOverlayContent(call)
1044
1104
  android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Content set for overlayView for call ${call.id}")
@@ -1050,6 +1110,7 @@ public class StreamCallPlugin : Plugin() {
1050
1110
  parent?.removeView(overlayView)
1051
1111
  parent?.addView(overlayView, 0) // Add at index 0 to ensure it's behind other views
1052
1112
  android.util.Log.d("StreamCallPlugin", "internalAcceptCall: OverlayView re-added to parent at index 0 for call ${call.id}")
1113
+
1053
1114
  // Add a small delay to ensure UI refresh
1054
1115
  mainHandler.postDelayed({
1055
1116
  android.util.Log.d("StreamCallPlugin", "internalAcceptCall: Delayed UI check, overlay visible: ${overlayView?.isVisible} for call ${call.id}")
@@ -1071,6 +1132,19 @@ public class StreamCallPlugin : Plugin() {
1071
1132
  }
1072
1133
  }, 1000) // Increased delay to ensure all events are processed
1073
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
+
1074
1148
  } catch (e: Exception) {
1075
1149
  android.util.Log.e("StreamCallPlugin", "internalAcceptCall: Error accepting call ${call.id}: ${e.message}", e)
1076
1150
  runOnMainThread {
@@ -1096,61 +1170,481 @@ public class StreamCallPlugin : Plugin() {
1096
1170
  return allGranted
1097
1171
  }
1098
1172
 
1099
- // Function to request required permissions
1100
- private fun requestPermissions() {
1101
- android.util.Log.d("StreamCallPlugin", "requestPermissions: Requesting RECORD_AUDIO and CAMERA permissions.")
1102
- ActivityCompat.requestPermissions(
1103
- activity,
1104
- arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA),
1105
- 1001 // Request code for permission result handling
1106
- )
1107
- android.util.Log.d("StreamCallPlugin", "requestPermissions: ActivityCompat.requestPermissions called.")
1108
- }
1109
-
1110
1173
  // Override to handle permission results
1111
1174
  override fun handleRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
1112
1175
  super.handleRequestPermissionsResult(requestCode, permissions, grantResults)
1113
- android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Entered. RequestCode: $requestCode")
1114
- if (requestCode == 1001) {
1115
- android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Matched requestCode 1001.")
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
+
1116
1183
  logPermissionResults(permissions, grantResults)
1184
+
1117
1185
  if (grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
1118
1186
  android.util.Log.i("StreamCallPlugin", "handleRequestPermissionsResult: All permissions GRANTED.")
1119
- // Permissions granted, can attempt to join the call again if needed
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
1120
1301
  val ringingCall = streamVideoClient?.state?.ringingCall?.value
1121
- android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Ringing call object: ${ringingCall?.id}")
1122
1302
  if (ringingCall != null) {
1123
- android.util.Log.d("StreamCallPlugin", "handleRequestPermissionsResult: Ringing call found (${ringingCall.id}). Re-attempting internalAcceptCall.")
1303
+ android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Fallback - accepting current ringing call ${ringingCall.id}")
1124
1304
  kotlinx.coroutines.GlobalScope.launch {
1125
- internalAcceptCall(ringingCall)
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
+ }
1126
1316
  }
1127
1317
  } else {
1128
- android.util.Log.w("StreamCallPlugin", "handleRequestPermissionsResult: Permissions granted, but no ringing call found to accept.")
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
1129
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()
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.")
1130
1504
  } else {
1131
- android.util.Log.e("StreamCallPlugin", "handleRequestPermissionsResult: One or more permissions DENIED.")
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
+
1132
1627
  runOnMainThread {
1133
1628
  android.widget.Toast.makeText(
1134
1629
  context,
1135
- "Permissions not granted. Cannot join call.",
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",
1136
1640
  android.widget.Toast.LENGTH_LONG
1137
1641
  ).show()
1138
1642
  }
1139
1643
  }
1140
- } else {
1141
- android.util.Log.w("StreamCallPlugin", "handleRequestPermissionsResult: Received unknown requestCode: $requestCode")
1142
1644
  }
1143
1645
  }
1144
1646
 
1145
- private fun logPermissionResults(permissions: Array<out String>, grantResults: IntArray) {
1146
- android.util.Log.d("StreamCallPlugin", "logPermissionResults: Logging permission results:")
1147
- for (i in permissions.indices) {
1148
- val permission = permissions[i]
1149
- val grantResult = if (grantResults.size > i) grantResults[i] else -999 // -999 for safety if arrays mismatch
1150
- val resultString = if (grantResult == PackageManager.PERMISSION_GRANTED) "GRANTED" else "DENIED ($grantResult)"
1151
- android.util.Log.d("StreamCallPlugin", " Permission: $permission, Result: $resultString")
1152
- }
1153
- }
1647
+
1154
1648
 
1155
1649
  @OptIn(DelicateCoroutinesApi::class)
1156
1650
  @PluginMethod
@@ -1455,58 +1949,21 @@ public class StreamCallPlugin : Plugin() {
1455
1949
 
1456
1950
  // Check permissions before creating the call
1457
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
1458
1961
  requestPermissions()
1459
- call.reject("Permissions required for call. Please grant them.")
1460
- return
1962
+ return // Don't reject immediately, wait for permission result
1461
1963
  }
1462
1964
 
1463
- // Create and join call in a coroutine
1464
- kotlinx.coroutines.GlobalScope.launch {
1465
- try {
1466
- // Create the call object
1467
- val streamCall = streamVideoClient?.call(type = callType, id = callId)
1468
-
1469
- // Note: We no longer start tracking here - we'll wait for CallSessionStartedEvent
1470
- // instead, which contains the actual participant list
1471
-
1472
- android.util.Log.d("StreamCallPlugin", "Creating call with members...")
1473
- // Create the call with all members
1474
- val createResult = streamCall?.create(
1475
- memberIds = userIds + selfUserId,
1476
- custom = emptyMap(),
1477
- ring = shouldRing,
1478
- team = team,
1479
- )
1480
-
1481
- if (createResult?.isFailure == true) {
1482
- throw (createResult.errorOrNull() ?: RuntimeException("Unknown error creating call")) as Throwable
1483
- }
1484
-
1485
- android.util.Log.d("StreamCallPlugin", "Setting overlay visible for outgoing call $callId")
1486
- // Show overlay view
1487
- activity?.runOnUiThread {
1488
- streamCall?.microphone?.setEnabled(true)
1489
- streamCall?.camera?.setEnabled(true)
1490
-
1491
- bridge?.webView?.setBackgroundColor(Color.TRANSPARENT) // Make webview transparent
1492
- bridge?.webView?.bringToFront() // Ensure WebView is on top and transparent
1493
- setOverlayContent(streamCall)
1494
- overlayView?.isVisible = true
1495
- // Ensure overlay is behind WebView by adjusting its position in the parent
1496
- val parent = overlayView?.parent as? ViewGroup
1497
- parent?.removeView(overlayView)
1498
- parent?.addView(overlayView, 0) // Add at index 0 to ensure it's behind other views
1499
- }
1500
-
1501
- // Resolve the call with success
1502
- call.resolve(JSObject().apply {
1503
- put("success", true)
1504
- })
1505
- } catch (e: Exception) {
1506
- android.util.Log.e("StreamCallPlugin", "Error making call: ${e.message}")
1507
- call.reject("Failed to make call: ${e.message}")
1508
- }
1509
- }
1965
+ // Execute call creation immediately if permissions are granted
1966
+ createAndStartCall(call, userIds, callType, shouldRing, team)
1510
1967
  } catch (e: Exception) {
1511
1968
  call.reject("Failed to make call: ${e.message}")
1512
1969
  }
@@ -1722,7 +2179,7 @@ public class StreamCallPlugin : Plugin() {
1722
2179
  call.reject("No active call")
1723
2180
  }
1724
2181
  }
1725
-
2182
+
1726
2183
  // Helper method to update call status and notify listeners
1727
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) {
1728
2185
  android.util.Log.d("StreamCallPlugin", "updateCallStatusAndNotify called: callId=$callId, state=$state, userId=$userId, reason=$reason")
@@ -1820,7 +2277,7 @@ public class StreamCallPlugin : Plugin() {
1820
2277
  val call = streamVideoClient?.call(id = cid.id, type = cid.type)
1821
2278
  if (call != null) {
1822
2279
  kotlinx.coroutines.GlobalScope.launch {
1823
- internalAcceptCall(call)
2280
+ internalAcceptCall(call, requestPermissionsAfter = !checkPermissions())
1824
2281
  }
1825
2282
  bringAppToForeground()
1826
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-stream-call",
3
- "version": "0.0.70",
3
+ "version": "0.0.71",
4
4
  "description": "Uses the https://getstream.io/ SDK to implement calling in Capacitor",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",