@capgo/capacitor-stream-call 0.0.27 → 0.0.28

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
@@ -128,11 +128,14 @@ The SDK will automatically use the system language and these translations.
128
128
  * [`setMicrophoneEnabled(...)`](#setmicrophoneenabled)
129
129
  * [`setCameraEnabled(...)`](#setcameraenabled)
130
130
  * [`addListener('callEvent', ...)`](#addlistenercallevent-)
131
+ * [`addListener('incomingCall', ...)`](#addlistenerincomingcall-)
131
132
  * [`removeAllListeners()`](#removealllisteners)
132
133
  * [`acceptCall()`](#acceptcall)
133
134
  * [`rejectCall()`](#rejectcall)
134
135
  * [`isCameraEnabled()`](#iscameraenabled)
135
136
  * [`getCallStatus()`](#getcallstatus)
137
+ * [`setSpeaker(...)`](#setspeaker)
138
+ * [`switchCamera(...)`](#switchcamera)
136
139
  * [Interfaces](#interfaces)
137
140
  * [Type Aliases](#type-aliases)
138
141
  * [Enums](#enums)
@@ -254,6 +257,25 @@ Add listener for call events
254
257
  --------------------
255
258
 
256
259
 
260
+ ### addListener('incomingCall', ...)
261
+
262
+ ```typescript
263
+ addListener(eventName: 'incomingCall', listenerFunc: (event: IncomingCallPayload) => void) => Promise<{ remove: () => Promise<void>; }>
264
+ ```
265
+
266
+ Listen for lock-screen incoming call (Android only).
267
+ Fired when the app is shown by full-screen intent before user interaction.
268
+
269
+ | Param | Type |
270
+ | ------------------ | --------------------------------------------------------------------------------------- |
271
+ | **`eventName`** | <code>'incomingCall'</code> |
272
+ | **`listenerFunc`** | <code>(event: <a href="#incomingcallpayload">IncomingCallPayload</a>) =&gt; void</code> |
273
+
274
+ **Returns:** <code>Promise&lt;{ remove: () =&gt; Promise&lt;void&gt;; }&gt;</code>
275
+
276
+ --------------------
277
+
278
+
257
279
  ### removeAllListeners()
258
280
 
259
281
  ```typescript
@@ -317,6 +339,40 @@ Get the current call status
317
339
  --------------------
318
340
 
319
341
 
342
+ ### setSpeaker(...)
343
+
344
+ ```typescript
345
+ setSpeaker(options: { name: string; }) => Promise<SuccessResponse>
346
+ ```
347
+
348
+ Set speakerphone on
349
+
350
+ | Param | Type | Description |
351
+ | ------------- | ------------------------------ | ------------------- |
352
+ | **`options`** | <code>{ name: string; }</code> | - Speakerphone name |
353
+
354
+ **Returns:** <code>Promise&lt;<a href="#successresponse">SuccessResponse</a>&gt;</code>
355
+
356
+ --------------------
357
+
358
+
359
+ ### switchCamera(...)
360
+
361
+ ```typescript
362
+ switchCamera(options: { camera: 'front' | 'back'; }) => Promise<SuccessResponse>
363
+ ```
364
+
365
+ Switch camera
366
+
367
+ | Param | Type | Description |
368
+ | ------------- | ------------------------------------------- | --------------------- |
369
+ | **`options`** | <code>{ camera: 'front' \| 'back'; }</code> | - Camera to switch to |
370
+
371
+ **Returns:** <code>Promise&lt;<a href="#successresponse">SuccessResponse</a>&gt;</code>
372
+
373
+ --------------------
374
+
375
+
320
376
  ### Interfaces
321
377
 
322
378
 
@@ -550,6 +606,14 @@ The JSON representation for <a href="#listvalue">`ListValue`</a> is JSON array.
550
606
  | **`sessionId`** | <code>string</code> | the user sesion_id to pin, if not provided, applies to all sessions |
551
607
 
552
608
 
609
+ #### IncomingCallPayload
610
+
611
+ | Prop | Type | Description |
612
+ | ---------- | ----------------------- | ---------------------------------------- |
613
+ | **`cid`** | <code>string</code> | Full call CID (e.g. default:123) |
614
+ | **`type`** | <code>'incoming'</code> | Event type (currently always "incoming") |
615
+
616
+
553
617
  #### CameraEnabledResponse
554
618
 
555
619
  | Prop | Type |
@@ -63,7 +63,7 @@ dependencies {
63
63
  implementation fileTree(dir: 'libs', include: ['*.jar'])
64
64
  implementation project(':capacitor-android')
65
65
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
66
- implementation 'androidx.core:core-ktx:1.15.0'
66
+ implementation 'androidx.core:core-ktx:1.16.0'
67
67
 
68
68
  // Compose dependencies with consistent versions
69
69
  def compose_version = '1.6.8'
@@ -72,20 +72,20 @@ dependencies {
72
72
  implementation "androidx.compose.ui:ui-tooling:$compose_version"
73
73
  implementation "androidx.compose.foundation:foundation:$compose_version"
74
74
  implementation "androidx.compose.runtime:runtime:$compose_version"
75
- implementation "androidx.compose.material3:material3:1.3.1"
75
+ implementation "androidx.compose.material3:material3:1.3.2"
76
76
 
77
77
  // Stream dependencies
78
- implementation("io.getstream:stream-video-android-ui-compose:1.4.5")
79
- implementation("io.getstream:stream-video-android-core:1.4.5")
78
+ implementation("io.getstream:stream-video-android-ui-compose:1.6.1")
79
+ implementation("io.getstream:stream-video-android-core:1.6.1")
80
80
  implementation("io.getstream:stream-android-push:1.3.1")
81
81
  implementation("io.getstream:stream-android-push-firebase:1.3.1")
82
82
 
83
83
  // Firebase dependencies using BOM
84
- implementation(platform('com.google.firebase:firebase-bom:32.8.0'))
84
+ implementation(platform('com.google.firebase:firebase-bom:33.13.0'))
85
85
  implementation('com.google.firebase:firebase-messaging-ktx')
86
86
 
87
87
  implementation("androidx.coordinatorlayout:coordinatorlayout:1.3.0")
88
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.1'
88
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.9.0'
89
89
  testImplementation "junit:junit:$junitVersion"
90
90
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
91
91
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -1,2 +1,31 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
3
+ <application>
4
+
5
+ <service
6
+ android:name="io.getstream.android.push.firebase.ChatFirebaseMessagingService"
7
+ android:exported="false">
8
+ <intent-filter>
9
+ <action android:name="com.google.firebase.MESSAGING_EVENT" />
10
+ </intent-filter>
11
+ <intent-filter>
12
+ <action android:name="FCM_PLUGIN_ACTIVITY" />
13
+ <category android:name="android.intent.category.DEFAULT" />
14
+ </intent-filter>
15
+ </service>
16
+
17
+ <service
18
+ android:name="ee.forgr.capacitor.streamcall.StreamCallBackgroundService"
19
+ android:enabled="true"
20
+ android:exported="false" />
21
+ </application>
22
+
23
+ <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
24
+ <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
25
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
26
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
27
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
28
+ <uses-permission android:name="android.permission.CAMERA" />
29
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
30
+ <uses-permission android:name="android.permission.BLUETOOTH" />
2
31
  </manifest>
@@ -4,13 +4,11 @@ import android.app.Application
4
4
  import android.app.Notification
5
5
  import android.app.NotificationManager
6
6
  import android.app.PendingIntent
7
- import android.content.Context
8
7
  import android.content.Intent
9
8
  import android.media.RingtoneManager
10
9
  import android.os.Build
11
10
  import android.util.Log
12
11
  import androidx.core.app.NotificationCompat
13
- import io.getstream.log.taggedLogger
14
12
  import io.getstream.video.android.core.RingingState
15
13
  import io.getstream.video.android.core.notifications.DefaultNotificationHandler
16
14
  import io.getstream.video.android.core.notifications.NotificationHandler
@@ -36,11 +34,66 @@ class CustomNotificationHandler(
36
34
  callDisplayName: String?,
37
35
  shouldHaveContentIntent: Boolean,
38
36
  ): Notification? {
37
+ Log.d("CustomNotificationHandler", "getRingingCallNotification called: ringingState=$ringingState, callId=$callId, callDisplayName=$callDisplayName, shouldHaveContentIntent=$shouldHaveContentIntent")
39
38
  return if (ringingState is RingingState.Incoming) {
40
- val fullScreenPendingIntent = intentResolver.searchIncomingCallPendingIntent(callId)
41
- val acceptCallPendingIntent = intentResolver.searchAcceptCallPendingIntent(callId)
42
- val rejectCallPendingIntent = intentResolver.searchRejectCallPendingIntent(callId)
43
-
39
+ // Note: we create our own fullScreenPendingIntent later based on acceptCallPendingIntent
40
+
41
+ // Get the main launch intent for the application
42
+ val launchIntent = application.packageManager.getLaunchIntentForPackage(application.packageName)
43
+ var targetComponent: android.content.ComponentName? = null
44
+ if (launchIntent != null) {
45
+ targetComponent = launchIntent.component
46
+ Log.d("CustomNotificationHandler", "Derived launch component: ${targetComponent?.flattenToString()}")
47
+ } else {
48
+ Log.e("CustomNotificationHandler", "Could not get launch intent for package: ${application.packageName}. This is problematic for creating explicit intents.")
49
+ }
50
+
51
+ // Intent to simply bring the app to foreground and show incoming-call UI (no auto accept)
52
+ val incomingIntentAction = "io.getstream.video.android.action.INCOMING_CALL"
53
+ val incomingCallIntent = Intent(incomingIntentAction)
54
+ .putExtra(NotificationHandler.INTENT_EXTRA_CALL_CID, callId)
55
+ .setPackage(application.packageName)
56
+ if (targetComponent != null) incomingCallIntent.component = targetComponent
57
+ incomingCallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
58
+
59
+ // Use the app's MainActivity intent so webview loads; user sees app UI
60
+ val requestCodeFull = callId.cid.hashCode()
61
+ val fullScreenPendingIntent = PendingIntent.getActivity(
62
+ application,
63
+ requestCodeFull,
64
+ incomingCallIntent,
65
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
66
+ )
67
+
68
+ val acceptCallAction = NotificationHandler.ACTION_ACCEPT_CALL
69
+ val acceptCallIntent = Intent(acceptCallAction)
70
+ // Pass full Parcelable so both new and old handlers succeed
71
+ .putExtra(NotificationHandler.INTENT_EXTRA_CALL_CID, callId)
72
+ .setPackage(application.packageName)
73
+
74
+ if (targetComponent != null) {
75
+ acceptCallIntent.component = targetComponent
76
+ }
77
+ acceptCallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
78
+
79
+ Log.d("CustomNotificationHandler", "Constructed Accept Call Intent for PI: action=${acceptCallIntent.action}, cid=${acceptCallIntent.getStringExtra(NotificationHandler.INTENT_EXTRA_CALL_CID)}, package=${acceptCallIntent.getPackage()}, component=${acceptCallIntent.component?.flattenToString()}, flags=${acceptCallIntent.flags}")
80
+
81
+ // Create PendingIntent for Accept action using getActivity to launch the app
82
+ val requestCodeAccept = callId.cid.hashCode() + 1 // Unique request code for the PendingIntent with offset to avoid collisions
83
+ val acceptCallPendingIntent = PendingIntent.getActivity(
84
+ application,
85
+ requestCodeAccept,
86
+ acceptCallIntent,
87
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
88
+ )
89
+ Log.d("CustomNotificationHandler", "Created Accept Call PendingIntent with requestCode: $requestCodeAccept")
90
+
91
+ val rejectCallPendingIntent = intentResolver.searchRejectCallPendingIntent(callId) // Keep using resolver for reject for now, or change it too if needed
92
+
93
+ Log.d("CustomNotificationHandler", "Full Screen PI: $fullScreenPendingIntent")
94
+ Log.d("CustomNotificationHandler", "Custom Accept Call PI: $acceptCallPendingIntent")
95
+ Log.d("CustomNotificationHandler", "Resolver Reject Call PI: $rejectCallPendingIntent")
96
+
44
97
  if (fullScreenPendingIntent != null && acceptCallPendingIntent != null && rejectCallPendingIntent != null) {
45
98
  customGetIncomingCallNotification(
46
99
  fullScreenPendingIntent,
@@ -81,37 +134,11 @@ class CustomNotificationHandler(
81
134
  shouldHaveContentIntent: Boolean,
82
135
  callId: StreamCallId
83
136
  ): Notification {
84
-
85
- // customCreateIncomingCallChannel()
86
- val manufacturer = Build.MANUFACTURER.lowercase()
87
- if (manufacturer.contains("xiaomi") || manufacturer.contains("mi")) {
88
- // val serviceIntent = Intent(application, CallForegroundService::class.java)
89
- // serviceIntent.action = CallForegroundService.ACTION_START_FOREGROUND_SERVICE
90
- // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
91
- // application.startForegroundService(serviceIntent)
92
- // } else {
93
- // application.startService(serviceIntent)
94
- // }
95
- // Adjust PendingIntent for Xiaomi to avoid permission denial
96
- val xiaomiAcceptIntent = PendingIntent.getActivity(
97
- application,
98
- 0,
99
- Intent("io.getstream.video.android.action.ACCEPT_CALL")
100
- .setPackage(application.packageName)
101
- .putExtra(NotificationHandler.INTENT_EXTRA_CALL_CID, callId),
102
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
103
- )
104
- return buildNotification(
105
- fullScreenPendingIntent,
106
- xiaomiAcceptIntent,
107
- rejectCallPendingIntent,
108
- callerName,
109
- shouldHaveContentIntent,
110
- INCOMING_CALLS_CUSTOM,
111
- true // Include sound
112
- )
113
- }
114
-
137
+ Log.d("CustomNotificationHandler", "customGetIncomingCallNotification called: callerName=$callerName, callId=$callId")
138
+ customCreateIncomingCallChannel()
139
+ // Always use the provided acceptCallPendingIntent (created with getActivity) so that
140
+ // the app process is started and MainActivity receives the ACCEPT_CALL action even
141
+ // when the app has been killed.
115
142
  return buildNotification(
116
143
  fullScreenPendingIntent,
117
144
  acceptCallPendingIntent,
@@ -132,10 +159,11 @@ class CustomNotificationHandler(
132
159
  channelId: String,
133
160
  includeSound: Boolean
134
161
  ): Notification {
162
+ Log.d("CustomNotificationHandler", "buildNotification called: callerName=$callerName, channelId=$channelId, includeSound=$includeSound")
135
163
  return getNotification {
136
164
  priority = NotificationCompat.PRIORITY_HIGH
137
165
  setContentTitle(callerName)
138
- setContentText("Incoming call")
166
+ setContentText("Incoming call toto")
139
167
  setChannelId(channelId)
140
168
  setOngoing(true)
141
169
  setAutoCancel(false)
@@ -173,11 +201,13 @@ class CustomNotificationHandler(
173
201
  }
174
202
 
175
203
  override fun onMissedCall(callId: StreamCallId, callDisplayName: String) {
204
+ Log.d("CustomNotificationHandler", "onMissedCall called: callId=$callId, callDisplayName=$callDisplayName")
176
205
  endCall(callId)
177
206
  super.onMissedCall(callId, callDisplayName)
178
207
  }
179
208
 
180
209
  private fun customCreateIncomingCallChannel() {
210
+ Log.d("CustomNotificationHandler", "customCreateIncomingCallChannel called")
181
211
  maybeCreateChannel(
182
212
  channelId = INCOMING_CALLS_CUSTOM,
183
213
  context = application,
@@ -204,6 +234,7 @@ class CustomNotificationHandler(
204
234
  }
205
235
 
206
236
  public fun clone(): CustomNotificationHandler {
237
+ Log.d("CustomNotificationHandler", "clone called")
207
238
  return CustomNotificationHandler(this.application, this.endCall, this.incomingCall)
208
239
  }
209
240
  }
@@ -0,0 +1,41 @@
1
+ package ee.forgr.capacitor.streamcall;
2
+
3
+ import android.app.Service;
4
+ import android.content.Intent;
5
+ import android.os.IBinder;
6
+ import android.util.Log;
7
+ import androidx.annotation.Nullable;
8
+
9
+ public class StreamCallBackgroundService extends Service {
10
+
11
+ private static final String TAG = "StreamCallBackgroundService";
12
+
13
+ @Override
14
+ public void onCreate() {
15
+ super.onCreate();
16
+ Log.d(TAG, "Service created");
17
+ }
18
+
19
+ @Override
20
+ public int onStartCommand(Intent intent, int flags, int startId) {
21
+ Log.d(TAG, "Service started");
22
+ // Keep the service running even if the app is killed
23
+ return START_STICKY;
24
+ }
25
+
26
+ @Override
27
+ public void onDestroy() {
28
+ super.onDestroy();
29
+ Log.d(TAG, "Service destroyed");
30
+ // Restart the service if it's killed by the system
31
+ Intent restartServiceIntent = new Intent(getApplicationContext(), StreamCallBackgroundService.class);
32
+ restartServiceIntent.setPackage(getPackageName());
33
+ startService(restartServiceIntent);
34
+ }
35
+
36
+ @Nullable
37
+ @Override
38
+ public IBinder onBind(Intent intent) {
39
+ return null;
40
+ }
41
+ }
@@ -0,0 +1,56 @@
1
+ package ee.forgr.capacitor.streamcall;
2
+
3
+ import android.os.Bundle;
4
+ import android.view.LayoutInflater;
5
+ import android.view.View;
6
+ import android.view.ViewGroup;
7
+ import androidx.fragment.app.Fragment;
8
+ import io.getstream.video.android.core.Call;
9
+ import kotlinx.coroutines.CoroutineScope;
10
+ import kotlinx.coroutines.Dispatchers;
11
+ import kotlinx.coroutines.flow.launchIn;
12
+ import kotlinx.coroutines.flow.onEach;
13
+ import kotlinx.coroutines.launch;
14
+ import io.getstream.webrtc.android.ui.VideoTextureViewRenderer;
15
+ import stream.video.sfu.models.TrackType;
16
+
17
+ class StreamCallFragment : Fragment() {
18
+ private var call: Call? = null
19
+ private var videoRenderer: VideoTextureViewRenderer? = null
20
+
21
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
22
+ videoRenderer = VideoTextureViewRenderer(requireContext())
23
+ return videoRenderer!!
24
+ }
25
+
26
+ fun setCall(call: Call) {
27
+ this.call = call
28
+ videoRenderer?.let { renderer ->
29
+ // Setup video rendering for local video
30
+ call.initRenderer(renderer, call.sessionId, TrackType.TRACK_TYPE_VIDEO)
31
+ // Setup listener for remote participants' video
32
+ val scope = CoroutineScope(Dispatchers.Main)
33
+ scope.launch {
34
+ call.state.participants.onEach { participantStates ->
35
+ participantStates.forEach { participantState ->
36
+ participantState.videoTrack.onEach { videoTrack ->
37
+ videoTrack?.let { track ->
38
+ // Additional renderers might be needed for multiple participants
39
+ track.video.addSink(renderer)
40
+ }
41
+ }.launchIn(scope)
42
+ }
43
+ }.launchIn(scope)
44
+ }
45
+ }
46
+ }
47
+
48
+ fun getCall(): Call? {
49
+ return call
50
+ }
51
+
52
+ override fun onDestroyView() {
53
+ super.onDestroyView()
54
+ videoRenderer = null
55
+ }
56
+ }