Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions demo-app/src/main/kotlin/io/getstream/video/android/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import android.app.Application
import android.content.Context
import dagger.hilt.android.HiltAndroidApp
import io.getstream.android.video.generated.models.CallEndedEvent
import io.getstream.android.video.generated.models.CustomVideoEvent
import io.getstream.video.android.DemoCallJoinInterceptor.Companion.CALLEE_READY_TO_JOIN_EVENT_TYPE
import io.getstream.video.android.DemoCallJoinInterceptor.Companion.CALLER_READY_TO_JOIN_EVENT_TYPE
import io.getstream.video.android.core.StreamVideo
import io.getstream.video.android.core.moderations.CallModerationConstants
import io.getstream.video.android.data.model.PolicyViolationUiData
Expand All @@ -39,9 +42,19 @@ import kotlinx.coroutines.runBlocking
@HiltAndroidApp
class App : Application() {

companion object {
lateinit var demoApp: App
}

internal var policyViolationUiData: MutableStateFlow<PolicyViolationUiData?> =
MutableStateFlow(null)

public val callerReadyToJoinFlow = MutableStateFlow<CustomVideoEvent?>(null)
public val calleeReadyToJoinFlow = MutableStateFlow<CustomVideoEvent?>(null)

override fun onCreate() {
super.onCreate()

demoApp = this
// We use the provided StreamUserDataStore in the demo app for user data storage.
// This is a convenience class provided for storage but the SDK itself is not aware of
// this instance and doesn't use it. You can use it to store the logged in user and then
Expand All @@ -66,10 +79,31 @@ class App : Application() {
}

observePolicyViolation()
observeCallReadyToJoin()
}

internal var policyViolationUiData: MutableStateFlow<PolicyViolationUiData?> =
MutableStateFlow(null)
private fun observeCallReadyToJoin() {
CoroutineScope(Dispatchers.Default).launch {
StreamVideo.instanceState
.flatMapLatest { instance ->
instance?.state?.ringingCall ?: flowOf(null)
}.filterNotNull()
.collectLatest { call ->
call.events.collectLatest { event ->
if (event is CustomVideoEvent) {
when (event.custom["type"]) {
CALLER_READY_TO_JOIN_EVENT_TYPE -> {
callerReadyToJoinFlow.value = event
}
CALLEE_READY_TO_JOIN_EVENT_TYPE -> {
calleeReadyToJoinFlow.value = event
}
}
}
}
}
}
}

private fun observePolicyViolation() {
CoroutineScope(Dispatchers.Default).launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import io.getstream.video.android.compose.ui.ComposeStreamCallActivity
import io.getstream.video.android.compose.ui.StreamCallActivityComposeDelegate
import io.getstream.video.android.core.Call
import io.getstream.video.android.core.MemberState
import io.getstream.video.android.core.RingingState
import io.getstream.video.android.core.StreamVideo
import io.getstream.video.android.core.call.state.CallAction
import io.getstream.video.android.datastore.delegate.StreamUserDataStore
Expand All @@ -45,12 +46,29 @@ import io.getstream.video.android.ui.common.StreamCallActivityConfiguration
import io.getstream.video.android.ui.common.util.StreamCallActivityDelicateApi
import io.getstream.video.android.util.FullScreenCircleProgressBar
import io.getstream.video.android.util.StreamVideoInitHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.ConcurrentHashMap

@OptIn(StreamCallActivityDelicateApi::class)
class CallActivity : ComposeStreamCallActivity() {

companion object {
var USE_CALL_JOIN_INTERCEPTOR = false
}
Comment thread
rahul-lohra marked this conversation as resolved.

override val uiDelegate: StreamActivityUiDelegate<StreamCallActivity> = StreamDemoUiDelegate()
var observeCallReadyToJoinJob: Job? = null
var observeRingingJob: Job? = null
private val previousRingingStates = ConcurrentHashMap.newKeySet<RingingState>()
override val callJoinInterceptor = DemoCallJoinInterceptor(previousRingingStates)

/**
* This code is required to pass the UI-tests (as it hardcodes the configuration)
Expand All @@ -61,6 +79,27 @@ class CallActivity : ComposeStreamCallActivity() {
.copy(closeScreenOnCallEnded = false, canSkipPermissionRationale = false)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
observeRingingState()
}

private fun observeRingingState() {
previousRingingStates.clear()
observeRingingJob?.cancel()
observeRingingJob = CoroutineScope(Dispatchers.Default).launch {
StreamVideo.instanceState
.flatMapLatest { instance ->
instance?.state?.ringingCall ?: flowOf(null)
}.filterNotNull()
.collectLatest { call ->
call.state.ringingState.collectLatest {
previousRingingStates.add(it)
}
}
}
}

@StreamCallActivityDelicateApi
override fun onPreCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
runBlocking {
Expand Down Expand Up @@ -163,4 +202,11 @@ class CallActivity : ComposeStreamCallActivity() {
}
}
}

override fun finish() {
super.finish()
observeCallReadyToJoinJob?.cancel()
observeRingingJob?.cancel()
previousRingingStates.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-video-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.video.android

import io.getstream.log.taggedLogger
import io.getstream.video.android.CallActivity.Companion.USE_CALL_JOIN_INTERCEPTOR
import io.getstream.video.android.core.Call
import io.getstream.video.android.core.CallJoinInterceptor
import io.getstream.video.android.core.RingingState
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first

/**
* Do the following changes before testing this flow
* Set [io.getstream.video.android.core.INTERCEPTOR_TIMEOUT_MS] to 10_000L
* Set [io.getstream.video.android.core.PEER_CONNECTION_OBSERVER_TIMEOUT] to 10_000L
*/
class DemoCallJoinInterceptor(
private val previousRingingStates: Set<RingingState>,
) : CallJoinInterceptor {
private val logger by taggedLogger("DemoCallJoinInterceptor")

companion object {
const val CALLER_READY_TO_JOIN_EVENT_TYPE = "caller_ready_join"
const val CALLEE_READY_TO_JOIN_EVENT_TYPE = "callee_ready_join"
}

override suspend fun callReadyToJoin(call: Call) {
if (USE_CALL_JOIN_INTERCEPTOR) {
val isIncomingOrOutgoing = previousRingingStates.firstOrNull {
it is RingingState.Incoming || it is RingingState.Outgoing
}

val isOutgoing = isIncomingOrOutgoing is RingingState.Outgoing
val isIncoming = isIncomingOrOutgoing is RingingState.Incoming

val currentUserId = call.user.id
if (isIncoming) {
val result = call.sendCustomEvent(
mapOf(
"type" to CALLEE_READY_TO_JOIN_EVENT_TYPE,
"user_id" to currentUserId,
),
)
if (result.isSuccess) {
logger.d { "[callReadyToJoin] Successfully sent custom $CALLEE_READY_TO_JOIN_EVENT_TYPE event" }
App.demoApp.callerReadyToJoinFlow.filter { it != null && it.callCid == call.cid }.first()
} else {
logger.d { "[callReadyToJoin] Failed to send custom $CALLEE_READY_TO_JOIN_EVENT_TYPE event" }
}
logger.d { "[callReadyToJoin] callerReadyToJoinFlow finish" }
} else if (isOutgoing) {
val result = call.sendCustomEvent(
mapOf(
"type" to CALLER_READY_TO_JOIN_EVENT_TYPE,
"user_id" to currentUserId,
),
)

if (result.isSuccess) {
logger.d { "[callReadyToJoin] Successfully sent custom $CALLER_READY_TO_JOIN_EVENT_TYPE event" }
App.demoApp.calleeReadyToJoinFlow.filter { it != null && it.callCid == call.cid }.first()
} else {
logger.d { "[callReadyToJoin] Failed to send custom $CALLER_READY_TO_JOIN_EVENT_TYPE event" }
}
logger.d { "[callReadyToJoin] calleeReadyToJoinFlow finish" }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ fun AppNavHost(
composable(AppScreens.DirectCallJoin.route) {
val context = LocalContext.current
DirectCallJoinScreen(
navigateToDirectCall = { cid, members, joinAndRing ->
navigateToDirectCall = { cid, members, joinAndRing, useCallJoinInterceptor ->
CallActivity.USE_CALL_JOIN_INTERCEPTOR = useCallJoinInterceptor
context.startActivity(
StreamCallActivity.callIntent(
action = NotificationHandler.ACTION_OUTGOING_CALL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.getstream.video.android.R
import io.getstream.video.android.compose.theme.VideoTheme
import io.getstream.video.android.compose.ui.components.avatar.UserAvatar
import io.getstream.video.android.compose.ui.components.base.StreamButton
Expand All @@ -69,7 +70,12 @@ import java.util.UUID
@Composable
fun DirectCallJoinScreen(
viewModel: DirectCallJoinViewModel = hiltViewModel(),
navigateToDirectCall: (cid: StreamCallId, memberList: String, joinAndRing: Boolean) -> Unit,
navigateToDirectCall: (
cid: StreamCallId,
memberList: String,
joinAndRing: Boolean,
useCallJoinInterceptor: Boolean,
) -> Unit,
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

Expand Down Expand Up @@ -98,7 +104,7 @@ private fun Header(user: User?) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp) // Outer padding
.padding(start = 24.dp, end = 24.dp, top = 24.dp) // Outer padding
.padding(vertical = 12.dp), // Inner padding
verticalArrangement = Arrangement.Center,
) {
Expand Down Expand Up @@ -135,9 +141,15 @@ private fun Header(user: User?) {
private fun Body(
uiState: DirectCallUiState,
toggleUserSelection: (Int) -> Unit,
onStartCallClick: (cid: StreamCallId, membersList: String, joinAndRing: Boolean) -> Unit,
onStartCallClick: (
cid: StreamCallId,
membersList: String,
joinAndRing: Boolean,
useCallJoinInterceptor: Boolean,
) -> Unit,
) {
var callerJoinsFirst by rememberSaveable { mutableStateOf(true) }
var useCallJoinInterceptor by rememberSaveable { mutableStateOf(false) }

Box(
modifier = Modifier
Expand All @@ -161,11 +173,10 @@ private fun Body(
Row(
verticalAlignment = CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text("Join First", color = Color.White)
Text(stringResource(id = R.string.join_first), color = Color.White)
Checkbox(
callerJoinsFirst,
modifier = Modifier.offset(x = 10.dp),
Expand All @@ -179,6 +190,27 @@ private fun Body(
},
)
}
Row(
verticalAlignment = CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(stringResource(id = R.string.use_call_join_interceptor), color = Color.White)
Checkbox(
useCallJoinInterceptor,
modifier = Modifier.offset(x = 10.dp),
colors = CheckboxDefaults.colors(
uncheckedColor = Color.White, // Border color when unchecked
checkedColor = Color.White, // Fill color when checked
checkmarkColor = VideoTheme.colors.buttonBrandDefault, // Tick color
),
onCheckedChange = {
useCallJoinInterceptor = !useCallJoinInterceptor
},
)
}
UserList(
entries = users,
onUserClick = { clickedIndex -> toggleUserSelection(clickedIndex) },
Expand All @@ -204,11 +236,11 @@ private fun Body(
onClick = {
onStartCallClick(
StreamCallId("audio_call", UUID.randomUUID().toString()),
// StreamCallId("default", UUID.randomUUID().toString()),
users
.filter { it.isSelected }
.joinToString(separator = ",") { it.user.id ?: "" },
callerJoinsFirst,
useCallJoinInterceptor,
)
},
)
Expand All @@ -230,6 +262,7 @@ private fun Body(
.filter { it.isSelected }
.joinToString(separator = ",") { it.user.id ?: "" },
callerJoinsFirst,
useCallJoinInterceptor,
)
},
)
Expand Down Expand Up @@ -326,7 +359,7 @@ private fun HeaderPreview() {
},
),
toggleUserSelection = {},
) { _, _, _ ->
) { _, _, _, _ ->
}
}
}
2 changes: 2 additions & 0 deletions demo-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
<string name="policy_violation_message">We have to end your call as we have detected policy violation</string>
<string name="policy_violation_action_button">OK</string>
<string name="moderation_warning_title">Warning</string>
<string name="join_first">Join First</string>
<string name="use_call_join_interceptor">Use Call Join Interceptor</string>

<plurals name="chat_typing">
<item quantity="one">%s is typing</item>
Expand Down
18 changes: 14 additions & 4 deletions stream-video-android-core/api/stream-video-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -8593,10 +8593,10 @@ public final class io/getstream/video/android/core/Call {
public final fun isPinnedParticipant (Ljava/lang/String;)Z
public final fun isServerPin (Ljava/lang/String;)Z
public final fun isVideoEnabled ()Z
public final fun join (ZLio/getstream/video/android/core/CreateCallOptions;ZZLjava/lang/Boolean;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun join$default (Lio/getstream/video/android/core/Call;ZLio/getstream/video/android/core/CreateCallOptions;ZZLjava/lang/Boolean;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun joinAndRing (Ljava/util/List;Lio/getstream/video/android/core/CreateCallOptions;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun joinAndRing$default (Lio/getstream/video/android/core/Call;Ljava/util/List;Lio/getstream/video/android/core/CreateCallOptions;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun join (ZLio/getstream/video/android/core/CreateCallOptions;ZZLjava/lang/Boolean;Lio/getstream/video/android/core/CallJoinInterceptor;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun join$default (Lio/getstream/video/android/core/Call;ZLio/getstream/video/android/core/CreateCallOptions;ZZLjava/lang/Boolean;Lio/getstream/video/android/core/CallJoinInterceptor;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun joinAndRing (Ljava/util/List;Lio/getstream/video/android/core/CreateCallOptions;ZLio/getstream/video/android/core/CallJoinInterceptor;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun joinAndRing$default (Lio/getstream/video/android/core/Call;Ljava/util/List;Lio/getstream/video/android/core/CreateCallOptions;ZLio/getstream/video/android/core/CallJoinInterceptor;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun kickUser (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun kickUser$default (Lio/getstream/video/android/core/Call;Ljava/lang/String;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun leave (Ljava/lang/String;)V
Expand Down Expand Up @@ -8686,6 +8686,16 @@ public final class io/getstream/video/android/core/CallHealthMonitor {
public final fun stopTimer ()V
}

public final class io/getstream/video/android/core/CallJoinInterceptionException : java/lang/RuntimeException {
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getReason ()Ljava/lang/String;
}

public abstract interface class io/getstream/video/android/core/CallJoinInterceptor {
public abstract fun callReadyToJoin (Lio/getstream/video/android/core/Call;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class io/getstream/video/android/core/CallKt {
public static final field sfuReconnectTimeoutMillis I
}
Expand Down
Loading
Loading