@capgo/capacitor-stream-call 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +31 -0
- package/README.md +340 -0
- package/StreamCall.podspec +19 -0
- package/android/build.gradle +74 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CallOverlayView.kt +281 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomNotificationHandler.kt +142 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/IncomingCallView.kt +147 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/RingtonePlayer.kt +164 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +1014 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/TouchInterceptWrapper.kt +31 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/UserRepository.kt +111 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/values/strings.xml +7 -0
- package/dist/docs.json +533 -0
- package/dist/esm/definitions.d.ts +169 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +32 -0
- package/dist/esm/web.js +323 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +337 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +339 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/StreamCallPlugin/CallOverlayView.swift +147 -0
- package/ios/Sources/StreamCallPlugin/CustomCallParticipantImageView.swift +60 -0
- package/ios/Sources/StreamCallPlugin/CustomCallView.swift +257 -0
- package/ios/Sources/StreamCallPlugin/CustomVideoParticipantsView.swift +107 -0
- package/ios/Sources/StreamCallPlugin/ParticipantsView.swift +206 -0
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +722 -0
- package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +177 -0
- package/ios/Sources/StreamCallPlugin/UserRepository.swift +96 -0
- package/ios/Sources/StreamCallPlugin/WebviewNavigationDelegate.swift +68 -0
- package/ios/Tests/StreamCallPluginTests/StreamCallPluginTests.swift +15 -0
- package/package.json +96 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
package ee.forgr.capacitor.streamcall
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.widget.Toast
|
|
5
|
+
import androidx.compose.foundation.background
|
|
6
|
+
import androidx.compose.foundation.layout.Box
|
|
7
|
+
import androidx.compose.foundation.layout.BoxScope
|
|
8
|
+
import androidx.compose.foundation.layout.Column
|
|
9
|
+
import androidx.compose.foundation.layout.IntrinsicSize
|
|
10
|
+
import androidx.compose.foundation.layout.WindowInsets
|
|
11
|
+
import androidx.compose.foundation.layout.asPaddingValues
|
|
12
|
+
import androidx.compose.foundation.layout.defaultMinSize
|
|
13
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
14
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
15
|
+
import androidx.compose.foundation.layout.height
|
|
16
|
+
import androidx.compose.foundation.layout.heightIn
|
|
17
|
+
import androidx.compose.foundation.layout.padding
|
|
18
|
+
import androidx.compose.foundation.layout.safeDrawing
|
|
19
|
+
import androidx.compose.foundation.layout.width
|
|
20
|
+
import androidx.compose.foundation.layout.wrapContentHeight
|
|
21
|
+
import androidx.compose.foundation.lazy.LazyColumn
|
|
22
|
+
import androidx.compose.foundation.lazy.items
|
|
23
|
+
import androidx.compose.material3.Text
|
|
24
|
+
import androidx.compose.runtime.Composable
|
|
25
|
+
import androidx.compose.runtime.DisposableEffect
|
|
26
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
27
|
+
import androidx.compose.runtime.collectAsState
|
|
28
|
+
import androidx.compose.runtime.getValue
|
|
29
|
+
import androidx.compose.runtime.mutableStateOf
|
|
30
|
+
import androidx.compose.runtime.remember
|
|
31
|
+
import androidx.compose.runtime.setValue
|
|
32
|
+
import androidx.compose.ui.Alignment
|
|
33
|
+
import androidx.compose.ui.Modifier
|
|
34
|
+
import androidx.compose.ui.draw.clip
|
|
35
|
+
import androidx.compose.ui.layout.ContentScale
|
|
36
|
+
import androidx.compose.ui.layout.onSizeChanged
|
|
37
|
+
import androidx.compose.ui.platform.LocalConfiguration
|
|
38
|
+
import androidx.compose.ui.platform.LocalInspectionMode
|
|
39
|
+
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
40
|
+
import androidx.compose.ui.platform.LocalDensity
|
|
41
|
+
import androidx.compose.ui.text.style.TextAlign
|
|
42
|
+
import androidx.compose.ui.unit.IntSize
|
|
43
|
+
import androidx.compose.ui.unit.dp
|
|
44
|
+
import androidx.compose.ui.unit.sp
|
|
45
|
+
import io.getstream.video.android.compose.permission.LaunchCallPermissions
|
|
46
|
+
import io.getstream.video.android.compose.theme.VideoTheme
|
|
47
|
+
import io.getstream.video.android.compose.ui.components.call.renderer.FloatingParticipantVideo
|
|
48
|
+
import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantVideo
|
|
49
|
+
import io.getstream.video.android.compose.ui.components.call.renderer.ParticipantsLayout
|
|
50
|
+
import io.getstream.video.android.compose.ui.components.call.renderer.RegularVideoRendererStyle
|
|
51
|
+
import io.getstream.video.android.compose.ui.components.call.renderer.VideoRendererStyle
|
|
52
|
+
import io.getstream.video.android.compose.ui.components.call.renderer.copy
|
|
53
|
+
import io.getstream.video.android.core.GEO
|
|
54
|
+
import io.getstream.video.android.core.ParticipantState
|
|
55
|
+
import io.getstream.video.android.core.RealtimeConnection
|
|
56
|
+
import io.getstream.video.android.core.StreamVideo
|
|
57
|
+
import io.getstream.video.android.core.StreamVideoBuilder
|
|
58
|
+
import io.getstream.video.android.model.User
|
|
59
|
+
import io.getstream.video.android.core.Call
|
|
60
|
+
import io.getstream.video.android.compose.ui.components.video.VideoRenderer
|
|
61
|
+
import io.getstream.video.android.compose.ui.components.video.VideoScalingType
|
|
62
|
+
import io.getstream.video.android.compose.ui.components.video.config.VideoRendererConfig
|
|
63
|
+
import stream.video.sfu.models.TrackType
|
|
64
|
+
import androidx.compose.ui.graphics.Color
|
|
65
|
+
|
|
66
|
+
@Composable
|
|
67
|
+
private fun ParticipantVideoView(
|
|
68
|
+
call: Call,
|
|
69
|
+
participant: ParticipantState,
|
|
70
|
+
parentSize: IntSize,
|
|
71
|
+
onVisibilityChanged: ((ParticipantState, Boolean) -> Unit)? = null
|
|
72
|
+
) {
|
|
73
|
+
LaunchedEffect(participant) {
|
|
74
|
+
onVisibilityChanged?.invoke(participant, true)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
DisposableEffect(participant) {
|
|
78
|
+
onDispose {
|
|
79
|
+
onVisibilityChanged?.invoke(participant, false)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Box(
|
|
84
|
+
modifier = Modifier
|
|
85
|
+
.fillMaxWidth()
|
|
86
|
+
.height(300.dp)
|
|
87
|
+
.background(VideoTheme.colors.baseSenary),
|
|
88
|
+
contentAlignment = Alignment.Center
|
|
89
|
+
) {
|
|
90
|
+
ParticipantVideo(
|
|
91
|
+
modifier = Modifier
|
|
92
|
+
.fillMaxWidth()
|
|
93
|
+
.height(IntrinsicSize.Min),
|
|
94
|
+
call = call,
|
|
95
|
+
participant = participant
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@Composable
|
|
101
|
+
fun CallOverlayView(
|
|
102
|
+
context: Context,
|
|
103
|
+
streamVideo: StreamVideo?,
|
|
104
|
+
call: Call?
|
|
105
|
+
) {
|
|
106
|
+
if (streamVideo == null) {
|
|
107
|
+
Box(
|
|
108
|
+
modifier = Modifier
|
|
109
|
+
.fillMaxSize()
|
|
110
|
+
.background(Color.Red)
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Collect the active call state
|
|
116
|
+
//val activeCall by internalInstance.state.activeCall.collectAsState()
|
|
117
|
+
// val call = activeCall
|
|
118
|
+
if (call == null) {
|
|
119
|
+
Box(
|
|
120
|
+
modifier = Modifier
|
|
121
|
+
.fillMaxSize()
|
|
122
|
+
.background(Color.LightGray)
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle permissions in the Composable context
|
|
128
|
+
LaunchCallPermissions(
|
|
129
|
+
call = call,
|
|
130
|
+
onAllPermissionsGranted = {
|
|
131
|
+
try {
|
|
132
|
+
// Check session using reflection before joining
|
|
133
|
+
val callClass = call.javaClass
|
|
134
|
+
val sessionField = callClass.getDeclaredField("session")
|
|
135
|
+
sessionField.isAccessible = true
|
|
136
|
+
val sessionValue = sessionField.get(call)
|
|
137
|
+
|
|
138
|
+
if (sessionValue != null) {
|
|
139
|
+
android.util.Log.d("CallOverlayView", "Session already exists, skipping join")
|
|
140
|
+
} else {
|
|
141
|
+
android.util.Log.d("CallOverlayView", "No existing session, attempting to join call")
|
|
142
|
+
val result = call.join(create = true)
|
|
143
|
+
result.onError {
|
|
144
|
+
android.util.Log.d("CallOverlayView", "Error joining call")
|
|
145
|
+
Toast.makeText(context, it.message, Toast.LENGTH_LONG).show()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (e: Exception) {
|
|
149
|
+
android.util.Log.e("CallOverlayView", "Error checking session or joining call", e)
|
|
150
|
+
Toast.makeText(context, "Failed to join call: ${e.message}", Toast.LENGTH_LONG).show()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
// Apply VideoTheme
|
|
156
|
+
VideoTheme {
|
|
157
|
+
// Define required properties.
|
|
158
|
+
val allParticipants by call.state.participants.collectAsState()
|
|
159
|
+
val remoteParticipants = allParticipants.filter { !it.isLocal }
|
|
160
|
+
val remoteParticipantsCount by call.state.participantCounts.collectAsState()
|
|
161
|
+
val connection by call.state.connection.collectAsState()
|
|
162
|
+
val sessionId by call.state.session.collectAsState()
|
|
163
|
+
var parentSize: IntSize by remember { mutableStateOf(IntSize(0, 0)) }
|
|
164
|
+
|
|
165
|
+
// Add logging for debugging
|
|
166
|
+
LaunchedEffect(allParticipants, remoteParticipants, remoteParticipantsCount, connection, sessionId) {
|
|
167
|
+
android.util.Log.d("CallOverlayView", "Detailed State Update:")
|
|
168
|
+
android.util.Log.d("CallOverlayView", "- Call ID: ${call.id}")
|
|
169
|
+
android.util.Log.d("CallOverlayView", "- Session ID: ${sessionId?.id}")
|
|
170
|
+
android.util.Log.d("CallOverlayView", "- All Participants: $allParticipants")
|
|
171
|
+
android.util.Log.d("CallOverlayView", "- Remote Participants: $remoteParticipants")
|
|
172
|
+
android.util.Log.d("CallOverlayView", "- Remote Participant Count: $remoteParticipantsCount")
|
|
173
|
+
android.util.Log.d("CallOverlayView", "- Connection State: $connection")
|
|
174
|
+
|
|
175
|
+
// Log each participant's details
|
|
176
|
+
allParticipants.forEach { participant ->
|
|
177
|
+
android.util.Log.d("CallOverlayView", "Participant Details:")
|
|
178
|
+
android.util.Log.d("CallOverlayView", "- ID: ${participant.userId}")
|
|
179
|
+
android.util.Log.d("CallOverlayView", "- Is Local: ${participant.isLocal}")
|
|
180
|
+
android.util.Log.d("CallOverlayView", "- Has Video: ${participant.videoEnabled}")
|
|
181
|
+
android.util.Log.d("CallOverlayView", "- Has Audio: ${participant.audioEnabled}")
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
Box(
|
|
186
|
+
contentAlignment = Alignment.Center,
|
|
187
|
+
modifier = Modifier
|
|
188
|
+
.fillMaxSize()
|
|
189
|
+
.background(VideoTheme.colors.baseSenary)
|
|
190
|
+
.padding(WindowInsets.safeDrawing.asPaddingValues())
|
|
191
|
+
.onSizeChanged { parentSize = it }
|
|
192
|
+
) {
|
|
193
|
+
val videoRenderer: @Composable (
|
|
194
|
+
modifier: Modifier,
|
|
195
|
+
call: Call,
|
|
196
|
+
participant: ParticipantState,
|
|
197
|
+
style: VideoRendererStyle,
|
|
198
|
+
) -> Unit = { videoModifier, videoCall, videoParticipant, videoStyle ->
|
|
199
|
+
ParticipantVideo(
|
|
200
|
+
modifier = videoModifier,
|
|
201
|
+
call = videoCall,
|
|
202
|
+
participant = videoParticipant,
|
|
203
|
+
style = videoStyle,
|
|
204
|
+
scalingType = VideoScalingType.SCALE_ASPECT_FIT,
|
|
205
|
+
actionsContent = { _, _, _ -> }
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
val videoRendererNoAction: @Composable (ParticipantState) -> Unit =
|
|
209
|
+
{ participant ->
|
|
210
|
+
ParticipantVideo(
|
|
211
|
+
modifier = Modifier
|
|
212
|
+
.fillMaxSize()
|
|
213
|
+
.clip(VideoTheme.shapes.dialog),
|
|
214
|
+
call = call,
|
|
215
|
+
participant = participant,
|
|
216
|
+
style = RegularVideoRendererStyle(),
|
|
217
|
+
scalingType = VideoScalingType.SCALE_ASPECT_FIT,
|
|
218
|
+
actionsContent = { _, _, _ -> }
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
val floatingVideoRender: @Composable BoxScope.(
|
|
222
|
+
call: Call,
|
|
223
|
+
parentSize: IntSize
|
|
224
|
+
) -> Unit = { call, _ ->
|
|
225
|
+
val participants by call.state.participants.collectAsState()
|
|
226
|
+
val me = participants.firstOrNull { it.isLocal }
|
|
227
|
+
me?.let { localParticipant ->
|
|
228
|
+
val configuration = LocalConfiguration.current
|
|
229
|
+
val layoutDirection = LocalLayoutDirection.current
|
|
230
|
+
val density = LocalDensity.current
|
|
231
|
+
val safeDrawingPadding = WindowInsets.safeDrawing.asPaddingValues()
|
|
232
|
+
val adjustedSize = with(density) {
|
|
233
|
+
IntSize(
|
|
234
|
+
width = (configuration.screenWidthDp.dp.toPx() - safeDrawingPadding.calculateLeftPadding(layoutDirection).toPx() - safeDrawingPadding.calculateRightPadding(layoutDirection).toPx()).toInt(),
|
|
235
|
+
height = (configuration.screenHeightDp.dp.toPx() - safeDrawingPadding.calculateTopPadding().toPx() - safeDrawingPadding.calculateBottomPadding().toPx()).toInt()
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
FloatingParticipantVideo(
|
|
239
|
+
call = call,
|
|
240
|
+
videoRenderer = videoRendererNoAction,
|
|
241
|
+
participant = if (LocalInspectionMode.current) {
|
|
242
|
+
participants.first()
|
|
243
|
+
} else {
|
|
244
|
+
localParticipant
|
|
245
|
+
},
|
|
246
|
+
style = RegularVideoRendererStyle(),
|
|
247
|
+
parentBounds = adjustedSize,
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (remoteParticipants.isNotEmpty()) {
|
|
253
|
+
android.util.Log.d("CallOverlayView", "Showing ParticipantsLayout with ${remoteParticipants.size} remote participants")
|
|
254
|
+
ParticipantsLayout(
|
|
255
|
+
modifier = Modifier
|
|
256
|
+
.fillMaxSize(),
|
|
257
|
+
call = call,
|
|
258
|
+
videoRenderer = videoRenderer,
|
|
259
|
+
floatingVideoRenderer = floatingVideoRender
|
|
260
|
+
)
|
|
261
|
+
} else {
|
|
262
|
+
if (connection != RealtimeConnection.Connected) {
|
|
263
|
+
android.util.Log.d("CallOverlayView", "Showing waiting message - not connected")
|
|
264
|
+
Text(
|
|
265
|
+
text = "waiting for a remote participant...",
|
|
266
|
+
fontSize = 30.sp,
|
|
267
|
+
color = VideoTheme.colors.basePrimary
|
|
268
|
+
)
|
|
269
|
+
} else {
|
|
270
|
+
Text(
|
|
271
|
+
modifier = Modifier.padding(30.dp),
|
|
272
|
+
text = "Join call ${call.id} in your browser to see the video here",
|
|
273
|
+
fontSize = 30.sp,
|
|
274
|
+
color = VideoTheme.colors.basePrimary,
|
|
275
|
+
textAlign = TextAlign.Center
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
package ee.forgr.capacitor.streamcall
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import android.app.Notification
|
|
5
|
+
import android.app.NotificationManager
|
|
6
|
+
import android.app.PendingIntent
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.media.RingtoneManager
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import androidx.core.app.NotificationCompat
|
|
12
|
+
import io.getstream.video.android.core.notifications.DefaultNotificationHandler
|
|
13
|
+
import io.getstream.video.android.model.StreamCallId
|
|
14
|
+
|
|
15
|
+
class CustomNotificationHandler(
|
|
16
|
+
val application: Application,
|
|
17
|
+
private val endCall: (callId: StreamCallId) -> Unit = {},
|
|
18
|
+
private val incomingCall: () -> Unit = {}
|
|
19
|
+
) : DefaultNotificationHandler(application, hideRingingNotificationInForeground = false) {
|
|
20
|
+
companion object {
|
|
21
|
+
private const val PREFS_NAME = "StreamCallPrefs"
|
|
22
|
+
private const val KEY_NOTIFICATION_TIME = "notification_creation_time"
|
|
23
|
+
}
|
|
24
|
+
var allowSound = true;
|
|
25
|
+
|
|
26
|
+
override fun getIncomingCallNotification(
|
|
27
|
+
fullScreenPendingIntent: PendingIntent,
|
|
28
|
+
acceptCallPendingIntent: PendingIntent,
|
|
29
|
+
rejectCallPendingIntent: PendingIntent,
|
|
30
|
+
callerName: String?,
|
|
31
|
+
shouldHaveContentIntent: Boolean,
|
|
32
|
+
): Notification {
|
|
33
|
+
val showAsHighPriority = true
|
|
34
|
+
val channelId = "incoming_calls_custom"
|
|
35
|
+
|
|
36
|
+
customCreateIncomingCallChannel(channelId, showAsHighPriority)
|
|
37
|
+
|
|
38
|
+
return buildNotification(
|
|
39
|
+
fullScreenPendingIntent,
|
|
40
|
+
acceptCallPendingIntent,
|
|
41
|
+
rejectCallPendingIntent,
|
|
42
|
+
callerName,
|
|
43
|
+
shouldHaveContentIntent,
|
|
44
|
+
channelId,
|
|
45
|
+
true // Include sound
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fun buildNotification(
|
|
50
|
+
fullScreenPendingIntent: PendingIntent,
|
|
51
|
+
acceptCallPendingIntent: PendingIntent,
|
|
52
|
+
rejectCallPendingIntent: PendingIntent,
|
|
53
|
+
callerName: String?,
|
|
54
|
+
shouldHaveContentIntent: Boolean,
|
|
55
|
+
channelId: String,
|
|
56
|
+
includeSound: Boolean
|
|
57
|
+
): Notification {
|
|
58
|
+
return getNotification {
|
|
59
|
+
priority = NotificationCompat.PRIORITY_HIGH
|
|
60
|
+
setContentTitle(callerName)
|
|
61
|
+
setContentText("Incoming call")
|
|
62
|
+
setChannelId(channelId)
|
|
63
|
+
setOngoing(true)
|
|
64
|
+
setAutoCancel(false)
|
|
65
|
+
setCategory(NotificationCompat.CATEGORY_CALL)
|
|
66
|
+
|
|
67
|
+
// Clear all defaults first
|
|
68
|
+
setDefaults(0)
|
|
69
|
+
|
|
70
|
+
if (includeSound && allowSound) {
|
|
71
|
+
setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE))
|
|
72
|
+
setDefaults(NotificationCompat.DEFAULT_VIBRATE or NotificationCompat.DEFAULT_LIGHTS)
|
|
73
|
+
} else {
|
|
74
|
+
setSound(null)
|
|
75
|
+
setDefaults(NotificationCompat.DEFAULT_VIBRATE or NotificationCompat.DEFAULT_LIGHTS)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setVibrate(longArrayOf(0, 1000, 500, 1000))
|
|
79
|
+
setLights(0xFF0000FF.toInt(), 1000, 1000)
|
|
80
|
+
setFullScreenIntent(fullScreenPendingIntent, true)
|
|
81
|
+
if (shouldHaveContentIntent) {
|
|
82
|
+
setContentIntent(fullScreenPendingIntent)
|
|
83
|
+
} else {
|
|
84
|
+
val emptyIntent = PendingIntent.getActivity(
|
|
85
|
+
application,
|
|
86
|
+
0,
|
|
87
|
+
Intent(),
|
|
88
|
+
PendingIntent.FLAG_IMMUTABLE,
|
|
89
|
+
)
|
|
90
|
+
setContentIntent(emptyIntent)
|
|
91
|
+
}
|
|
92
|
+
addCallActions(acceptCallPendingIntent, rejectCallPendingIntent, callerName)
|
|
93
|
+
}.apply {
|
|
94
|
+
// flags = flags or NotificationCompat.FLAG_ONGOING_EVENT
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun onMissedCall(callId: StreamCallId, callDisplayName: String) {
|
|
99
|
+
endCall(callId)
|
|
100
|
+
super.onMissedCall(callId, callDisplayName)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
open fun customCreateIncomingCallChannel(channelId: String, showAsHighPriority: Boolean) {
|
|
104
|
+
maybeCreateChannel(
|
|
105
|
+
channelId = channelId,
|
|
106
|
+
context = application,
|
|
107
|
+
configure = {
|
|
108
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
109
|
+
name = application.getString(
|
|
110
|
+
R.string.stream_video_incoming_call_notification_channel_title,
|
|
111
|
+
)
|
|
112
|
+
description = application.getString(
|
|
113
|
+
if (showAsHighPriority) {
|
|
114
|
+
R.string.stream_video_incoming_call_notification_channel_description
|
|
115
|
+
} else {
|
|
116
|
+
R.string.stream_video_incoming_call_low_priority_notification_channel_description
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
importance = if (showAsHighPriority) {
|
|
120
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
121
|
+
} else {
|
|
122
|
+
NotificationManager.IMPORTANCE_LOW
|
|
123
|
+
}
|
|
124
|
+
this.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
125
|
+
this.setShowBadge(true)
|
|
126
|
+
|
|
127
|
+
// Set the channel to be silent since we handle sound via RingtonePlayer
|
|
128
|
+
setSound(null, null)
|
|
129
|
+
enableVibration(true)
|
|
130
|
+
enableLights(true)
|
|
131
|
+
}
|
|
132
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
133
|
+
this.setAllowBubbles(true)
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public fun clone(): CustomNotificationHandler {
|
|
140
|
+
return CustomNotificationHandler(this.application, this.endCall, this.incomingCall)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
package ee.forgr.capacitor.streamcall
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import androidx.compose.foundation.background
|
|
5
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
6
|
+
import androidx.compose.foundation.layout.Box
|
|
7
|
+
import androidx.compose.foundation.layout.Column
|
|
8
|
+
import androidx.compose.foundation.layout.WindowInsets
|
|
9
|
+
import androidx.compose.foundation.layout.asPaddingValues
|
|
10
|
+
import androidx.compose.foundation.layout.calculateEndPadding
|
|
11
|
+
import androidx.compose.foundation.layout.calculateStartPadding
|
|
12
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
13
|
+
import androidx.compose.foundation.layout.navigationBars
|
|
14
|
+
import androidx.compose.foundation.layout.padding
|
|
15
|
+
import androidx.compose.foundation.layout.safeDrawing
|
|
16
|
+
import androidx.compose.foundation.layout.statusBars
|
|
17
|
+
import androidx.compose.runtime.Composable
|
|
18
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
19
|
+
import androidx.compose.runtime.collectAsState
|
|
20
|
+
import androidx.compose.runtime.getValue
|
|
21
|
+
import androidx.compose.ui.Alignment
|
|
22
|
+
import androidx.compose.ui.Modifier
|
|
23
|
+
import androidx.compose.ui.graphics.Color
|
|
24
|
+
import androidx.compose.ui.platform.LocalContext
|
|
25
|
+
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
26
|
+
import io.getstream.video.android.compose.theme.VideoTheme
|
|
27
|
+
import io.getstream.video.android.compose.ui.components.background.CallBackground
|
|
28
|
+
import io.getstream.video.android.compose.ui.components.call.ringing.incomingcall.IncomingCallControls
|
|
29
|
+
import io.getstream.video.android.compose.ui.components.call.ringing.incomingcall.IncomingCallDetails
|
|
30
|
+
import io.getstream.video.android.core.Call
|
|
31
|
+
import io.getstream.video.android.core.MemberState
|
|
32
|
+
import io.getstream.video.android.core.RingingState
|
|
33
|
+
import io.getstream.video.android.core.StreamVideo
|
|
34
|
+
import io.getstream.video.android.core.call.state.AcceptCall
|
|
35
|
+
import io.getstream.video.android.core.call.state.DeclineCall
|
|
36
|
+
import io.getstream.video.android.model.User
|
|
37
|
+
import java.time.OffsetDateTime
|
|
38
|
+
|
|
39
|
+
@Composable
|
|
40
|
+
fun IncomingCallView(
|
|
41
|
+
streamVideo: StreamVideo?,
|
|
42
|
+
call: Call? = null,
|
|
43
|
+
onDeclineCall: ((Call) -> Unit)? = null,
|
|
44
|
+
onAcceptCall: ((Call) -> Unit)? = null,
|
|
45
|
+
onHideIncomingCall: (() -> Unit)? = null
|
|
46
|
+
) {
|
|
47
|
+
val ringingState = call?.state?.ringingState?.collectAsState(initial = RingingState.Idle)
|
|
48
|
+
|
|
49
|
+
LaunchedEffect(ringingState?.value) {
|
|
50
|
+
Log.d("IncomingCallView", "Changing ringingState to $ringingState?.value")
|
|
51
|
+
if (ringingState?.value == RingingState.TimeoutNoAnswer || ringingState?.value == RingingState.RejectedByAll) {
|
|
52
|
+
Log.d("IncomingCallView", "Call timed out, hiding incoming call view")
|
|
53
|
+
onHideIncomingCall?.invoke()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (ringingState != null) {
|
|
58
|
+
Log.d("IncomingCallView", "Ringing state changed to: ${ringingState.value}")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
val backgroundColor = when {
|
|
62
|
+
streamVideo == null -> Color.Cyan
|
|
63
|
+
call == null -> Color.Red
|
|
64
|
+
else -> Color.Green
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (call !== null) {
|
|
68
|
+
// val participants by call.state.participants.collectAsState()
|
|
69
|
+
// val members by call.state.members.collectAsState()
|
|
70
|
+
// call.state.session
|
|
71
|
+
val session by call.state.session.collectAsState()
|
|
72
|
+
val isCameraEnabled by call.camera.isEnabled.collectAsState()
|
|
73
|
+
val isVideoType = true
|
|
74
|
+
|
|
75
|
+
val statusBarPadding = WindowInsets.statusBars.asPaddingValues()
|
|
76
|
+
val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues()
|
|
77
|
+
val safeDrawingPadding = WindowInsets.safeDrawing.asPaddingValues()
|
|
78
|
+
val layoutDirection = LocalLayoutDirection.current
|
|
79
|
+
|
|
80
|
+
// println("participants: ${participants.map { it.name.value }} Members: ${members}")
|
|
81
|
+
|
|
82
|
+
VideoTheme {
|
|
83
|
+
CallBackground(
|
|
84
|
+
modifier = Modifier.fillMaxSize(),
|
|
85
|
+
) {
|
|
86
|
+
Column(
|
|
87
|
+
modifier = Modifier
|
|
88
|
+
.fillMaxSize()
|
|
89
|
+
.padding(
|
|
90
|
+
top = statusBarPadding.calculateTopPadding(),
|
|
91
|
+
bottom = navigationBarPadding.calculateBottomPadding()
|
|
92
|
+
),
|
|
93
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
94
|
+
verticalArrangement = Arrangement.SpaceBetween
|
|
95
|
+
) {
|
|
96
|
+
IncomingCallDetails(
|
|
97
|
+
modifier = Modifier
|
|
98
|
+
.padding(
|
|
99
|
+
top = VideoTheme.dimens.spacingXl,
|
|
100
|
+
start = safeDrawingPadding.calculateStartPadding(layoutDirection),
|
|
101
|
+
end = safeDrawingPadding.calculateEndPadding(layoutDirection)
|
|
102
|
+
),
|
|
103
|
+
isVideoType = isVideoType,
|
|
104
|
+
participants = (session?.participants?.map { MemberState(
|
|
105
|
+
user = User(
|
|
106
|
+
id = it.user.id,
|
|
107
|
+
name = it.user.id,
|
|
108
|
+
image = it.user.image
|
|
109
|
+
),
|
|
110
|
+
custom = mapOf(),
|
|
111
|
+
role = it.role,
|
|
112
|
+
createdAt = org.threeten.bp.OffsetDateTime.now(),
|
|
113
|
+
updatedAt = org.threeten.bp.OffsetDateTime.now(),
|
|
114
|
+
deletedAt = org.threeten.bp.OffsetDateTime.now(),
|
|
115
|
+
acceptedAt = org.threeten.bp.OffsetDateTime.now(),
|
|
116
|
+
rejectedAt = org.threeten.bp.OffsetDateTime.now()
|
|
117
|
+
) }?.filter { it.user.id != streamVideo?.userId }) ?: listOf()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
IncomingCallControls(
|
|
121
|
+
modifier = Modifier
|
|
122
|
+
.padding(
|
|
123
|
+
bottom = VideoTheme.dimens.spacingL,
|
|
124
|
+
start = safeDrawingPadding.calculateStartPadding(layoutDirection),
|
|
125
|
+
end = safeDrawingPadding.calculateEndPadding(layoutDirection)
|
|
126
|
+
),
|
|
127
|
+
isVideoCall = isVideoType,
|
|
128
|
+
isCameraEnabled = isCameraEnabled,
|
|
129
|
+
onCallAction = { action ->
|
|
130
|
+
when (action) {
|
|
131
|
+
DeclineCall -> onDeclineCall?.invoke(call)
|
|
132
|
+
AcceptCall -> onAcceptCall?.invoke(call)
|
|
133
|
+
else -> { /* ignore other actions */ }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
Box(
|
|
142
|
+
modifier = Modifier
|
|
143
|
+
.fillMaxSize()
|
|
144
|
+
.background(backgroundColor)
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|