@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 = android.content.Intent.FLAG_ACTIVITY_NEW_TASK
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, is CallSessionParticipantCountsUpdatedEvent -> {
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
- 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
+ }
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
- // Check and request permissions before joining the call
1005
- val permissionsGranted = checkPermissions()
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
- // Reusing the initialization logic from call method
1043
- call.microphone?.setEnabled(true)
1044
- call.camera?.setEnabled(true)
1045
- 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
+
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
- if (requestCode == 1001) {
1119
- 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
+
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
- // 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
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", "handleRequestPermissionsResult: Ringing call found (${ringingCall.id}). Re-attempting internalAcceptCall.")
1303
+ android.util.Log.d("StreamCallPlugin", "handlePermissionGranted: Fallback - accepting current ringing call ${ringingCall.id}")
1128
1304
  kotlinx.coroutines.GlobalScope.launch {
1129
- 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
+ }
1130
1316
  }
1131
1317
  } else {
1132
- 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
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
- 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
+
1136
1627
  runOnMainThread {
1137
1628
  android.widget.Toast.makeText(
1138
1629
  context,
1139
- "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",
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
- private fun logPermissionResults(permissions: Array<out String>, grantResults: IntArray) {
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
- call.reject("Permissions required for call. Please grant them.")
1464
- return
1962
+ return // Don't reject immediately, wait for permission result
1465
1963
  }
1466
1964
 
1467
- // Create and join call in a coroutine
1468
- kotlinx.coroutines.GlobalScope.launch {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-stream-call",
3
- "version": "0.0.69",
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",