diff --git a/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api b/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api index 519dbef654..c40a2279e7 100644 --- a/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api +++ b/stream-video-android-ui-compose/api/stream-video-android-ui-compose.api @@ -356,32 +356,29 @@ public final class io/getstream/video/android/compose/theme/VideoThemeKt { public final class io/getstream/video/android/compose/ui/ComposableSingletons$StreamCallActivityComposeDelegateKt { public static final field INSTANCE Lio/getstream/video/android/compose/ui/ComposableSingletons$StreamCallActivityComposeDelegateKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; - public static field lambda-2 Lkotlin/jvm/functions/Function2; - public static field lambda-3 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-3$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public class io/getstream/video/android/compose/ui/ComposeStreamCallActivity : io/getstream/video/android/ui/common/StreamCallActivity { public static final field $stable I public fun ()V - public fun uiDelegate ()Lio/getstream/video/android/ui/common/StreamActivityUiDelegate; + public fun getUiDelegate ()Lio/getstream/video/android/ui/common/StreamActivityUiDelegate; } -public class io/getstream/video/android/compose/ui/StreamCallActivityComposeDelegate : io/getstream/video/android/ui/common/StreamActivityUiDelegate { +public class io/getstream/video/android/compose/ui/StreamCallActivityComposeDelegate : io/getstream/video/android/compose/ui/StreamCallActivityComposeUi { public static final field $stable I public static final field Companion Lio/getstream/video/android/compose/ui/StreamCallActivityComposeDelegate$Companion; public fun ()V public fun AudioCallContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V - public fun DefaultCallContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V + public fun IncomingCallContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;ZZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public fun LoadingContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V public fun NoAnswerContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V - public fun NoPermissions (Lio/getstream/video/android/ui/common/StreamCallActivity;Ljava/util/List;Ljava/util/List;ZLandroidx/compose/runtime/Composer;II)V - public fun OnIncomingCallContent (Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;ZZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)V - public fun OnOutgoingCallContent (Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;ZZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)V + public fun OutgoingCallContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;ZZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public fun PermissionsRationaleContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Ljava/util/List;Ljava/util/List;ZLandroidx/compose/runtime/Composer;I)V public fun RejectedContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V public fun RootContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V + public fun VideoCallContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V public fun loadingContent (Lio/getstream/video/android/ui/common/StreamCallActivity;)V public fun setContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;)V } @@ -389,6 +386,18 @@ public class io/getstream/video/android/compose/ui/StreamCallActivityComposeDele public final class io/getstream/video/android/compose/ui/StreamCallActivityComposeDelegate$Companion { } +public abstract interface class io/getstream/video/android/compose/ui/StreamCallActivityComposeUi : io/getstream/video/android/ui/common/StreamActivityUiDelegate { + public abstract fun AudioCallContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V + public abstract fun IncomingCallContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;ZZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public abstract fun LoadingContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V + public abstract fun NoAnswerContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V + public abstract fun OutgoingCallContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Landroidx/compose/ui/Modifier;Lio/getstream/video/android/core/Call;ZZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public abstract fun PermissionsRationaleContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Ljava/util/List;Ljava/util/List;ZLandroidx/compose/runtime/Composer;I)V + public abstract fun RejectedContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V + public abstract fun RootContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V + public abstract fun VideoCallContent (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Landroidx/compose/runtime/Composer;I)V +} + public final class io/getstream/video/android/compose/ui/components/audio/AudioAppBarKt { public static final fun AudioAppBar (Landroidx/compose/ui/Modifier;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)V } @@ -1582,16 +1591,18 @@ public final class io/getstream/video/android/compose/ui/components/call/ringing public static field lambda-3 Lkotlin/jvm/functions/Function2; public static field lambda-4 Lkotlin/jvm/functions/Function2; public static field lambda-5 Lkotlin/jvm/functions/Function2; + public static field lambda-6 Lkotlin/jvm/functions/Function2; public fun ()V public final fun getLambda-1$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-2$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-3$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-4$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda-5$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-6$stream_video_android_ui_compose_release ()Lkotlin/jvm/functions/Function2; } public final class io/getstream/video/android/compose/ui/components/call/ringing/RingingCallContentKt { - public static final fun RingingCallContent (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;ZZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function11;Lkotlin/jvm/functions/Function11;Landroidx/compose/runtime/Composer;III)V + public static final fun RingingCallContent (Lio/getstream/video/android/core/Call;Landroidx/compose/ui/Modifier;ZZLkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function11;Lkotlin/jvm/functions/Function11;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V } public final class io/getstream/video/android/compose/ui/components/call/ringing/incomingcall/ComposableSingletons$IncomingCallContentKt { diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/ComposeStreamCallActivity.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/ComposeStreamCallActivity.kt index f2e015c986..f63cb7cfad 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/ComposeStreamCallActivity.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/ComposeStreamCallActivity.kt @@ -25,8 +25,6 @@ import io.getstream.video.android.ui.common.StreamCallActivity */ public open class ComposeStreamCallActivity : StreamCallActivity() { - @Suppress("UNCHECKED_CAST") - override fun uiDelegate(): StreamActivityUiDelegate { - return StreamCallActivityComposeDelegate() as StreamActivityUiDelegate - } + override val uiDelegate: StreamActivityUiDelegate + get() = StreamCallActivityComposeDelegate() } diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/StreamCallActivityComposeDelegate.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/StreamCallActivityComposeDelegate.kt index 28c2316bb4..b28c2a7904 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/StreamCallActivityComposeDelegate.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/StreamCallActivityComposeDelegate.kt @@ -21,20 +21,32 @@ import android.content.Intent import android.net.Uri import android.provider.Settings import androidx.activity.compose.setContent +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.CircularProgressIndicator +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -57,11 +69,7 @@ import io.getstream.video.android.compose.ui.components.call.ringing.incomingcal import io.getstream.video.android.compose.ui.components.call.ringing.outgoingcall.OutgoingCallContent import io.getstream.video.android.core.Call import io.getstream.video.android.core.MemberState -import io.getstream.video.android.core.RealtimeConnection import io.getstream.video.android.core.call.state.CallAction -import io.getstream.video.android.core.call.state.DeclineCall -import io.getstream.video.android.core.call.state.LeaveCall -import io.getstream.video.android.ui.common.StreamActivityUiDelegate import io.getstream.video.android.ui.common.StreamCallActivity import io.getstream.video.android.ui.common.util.StreamCallActivityDelicateApi @@ -73,16 +81,18 @@ import io.getstream.video.android.ui.common.util.StreamCallActivityDelicateApi // We suppress build fails since we do not have default parameters in our abstract @Composable // Remove the @Suppress line once the listed issue is fixed. // https://issuetracker.google.com/issues/322121224 +@OptIn(StreamCallActivityDelicateApi::class) @Suppress("ABSTRACT_COMPOSABLE_DEFAULT_PARAMETER_VALUE") -public open class StreamCallActivityComposeDelegate : StreamActivityUiDelegate { +public open class StreamCallActivityComposeDelegate : StreamCallActivityComposeUi { public companion object { - private val logger by taggedLogger("StreamCallActivityUiDelegate") + private val logger by taggedLogger("StreamCallActivityComposeDelegate") } /** * Shows a progressbar until everything is set. */ + @StreamCallActivityDelicateApi override fun loadingContent(activity: StreamCallActivity) { activity.setContent { VideoTheme { @@ -91,9 +101,10 @@ public open class StreamCallActivityComposeDelegate : StreamActivityUiDelegate Unit)?, - detailsContent: @Composable ( - ColumnScope.( - participants: List, - topPadding: Dp, - ) -> Unit - )?, - controlsContent: @Composable (BoxScope.() -> Unit)?, - onBackPressed: () -> Unit, - onCallAction: (CallAction) -> Unit, + onOutgoingContent = { + modifier: Modifier, + call: Call, + isVideoType: Boolean, + isShowingHeader: Boolean, + headerContent: @Composable (ColumnScope.() -> Unit)?, + detailsContent: @Composable ( + ColumnScope.( + participants: List, + topPadding: Dp, + ) -> Unit + )?, + controlsContent: @Composable (BoxScope.() -> Unit)?, + onBackPressed: () -> Unit, + onCallAction: (CallAction) -> Unit, -> - OnOutgoingCallContent( + OutgoingCallContent( call = call, isVideoType = isVideoType, modifier = modifier, @@ -173,21 +167,22 @@ public open class StreamCallActivityComposeDelegate : StreamActivityUiDelegate Unit)?, - detailsContent: @Composable ( - ColumnScope.( - participants: List, - topPadding: Dp, - ) -> Unit - )?, - controlsContent: @Composable (BoxScope.() -> Unit)?, - onBackPressed: () -> Unit, - onCallAction: (CallAction) -> Unit, + onIncomingContent = { + modifier: Modifier, + call: Call, + isVideoType: Boolean, isShowingHeader: Boolean, + headerContent: @Composable (ColumnScope.() -> Unit)?, + detailsContent: @Composable ( + ColumnScope.( + participants: List, + topPadding: Dp, + ) -> Unit + )?, + controlsContent: @Composable (BoxScope.() -> Unit)?, + onBackPressed: () -> Unit, + onCallAction: (CallAction) -> Unit, -> - OnIncomingCallContent( + IncomingCallContent( call = call, isVideoType = isVideoType, modifier = modifier, @@ -201,7 +196,7 @@ public open class StreamCallActivityComposeDelegate : StreamActivityUiDelegate - // Some of the permissions were granted, you can check which ones. - if (showRationale) { - NoPermissions(granted, notGranted, true) - } else { - logger.w { "No permission, closing activity without rationale! [notGranted: [$notGranted]" } - finish() - } + PermissionsRationaleContent(call, granted, notGranted, showRationale) } + NoneGranted { - // None of the permissions were granted. - if (it) { - NoPermissions(showRationale = true) - } else { - logger.w { "No permission, closing activity without rationale!" } - finish() - } + PermissionsRationaleContent(call, emptyList(), emptyList(), it) } } } } @Composable - public open fun OnOutgoingCallContent( + override fun StreamCallActivity.LoadingContent(call: Call) { + Box( + modifier = Modifier + .fillMaxSize() + .background(VideoTheme.colors.baseSheetPrimary), + ) { + IndeterminateProgressBar( + modifier = Modifier + .align(Alignment.Center) + .padding(16.dp), + ) + } + } + + @Composable + override fun StreamCallActivity.AudioCallContent(call: Call) { + val micEnabled by call.microphone.isEnabled.collectAsStateWithLifecycle() + val duration by call.state.durationInDateFormat.collectAsStateWithLifecycle() + io.getstream.video.android.compose.ui.components.call.activecall.AudioCallContent( + onBackPressed = { + onBackPressed(call) + }, + call = call, + isMicrophoneEnabled = micEnabled, + onCallAction = { + onCallAction(call, it) + }, + durationPlaceholder = duration + ?: "...", + ) + } + + @Composable + override fun StreamCallActivity.VideoCallContent(call: Call) { + CallContent(call = call, onCallAction = { + onCallAction(call, it) + }, onBackPressed = { + onBackPressed(call) + }) + } + + @Composable + override fun StreamCallActivity.OutgoingCallContent( modifier: Modifier, call: Call, isVideoType: Boolean, @@ -257,7 +286,7 @@ public open class StreamCallActivityComposeDelegate : StreamActivityUiDelegate Unit, onCallAction: (CallAction) -> Unit, ) { - OutgoingCallContent( + io.getstream.video.android.compose.ui.components.call.ringing.outgoingcall.OutgoingCallContent( call = call, isVideoType = isVideoType, modifier = modifier, @@ -271,7 +300,7 @@ public open class StreamCallActivityComposeDelegate : StreamActivityUiDelegate Unit, onCallAction: (CallAction) -> Unit, ) { - IncomingCallContent( + io.getstream.video.android.compose.ui.components.call.ringing.incomingcall.IncomingCallContent( call = call, isVideoType = isVideoType, modifier = modifier, @@ -300,73 +329,33 @@ public open class StreamCallActivityComposeDelegate : StreamActivityUiDelegate = emptyList(), - notGranted: List = emptyList(), + override fun StreamCallActivity.PermissionsRationaleContent( + call: Call, + granted: List, + notGranted: List, showRationale: Boolean, ) { + if (!showRationale) { + logger.w { "Permissions were not granted, but rationale is required to be skipped." } + finish() + return + } + // Proceed as normal StreamDialogPositiveNegative( content = { Text( @@ -413,4 +402,41 @@ public open class StreamCallActivityComposeDelegate : StreamActivityUiDelegate { + + /** + * Root content of the UI. If you override this, the rest might not work as expected and need to + * be called from this method. + * + * @param call the call. + */ + @StreamCallActivityDelicateApi + @Composable + public fun StreamCallActivity.RootContent(call: Call) + + /** + * Content shown when there is data to be loaded. + * + * @param call the call. + */ + @Composable + public fun StreamCallActivity.LoadingContent(call: Call) + + /** + * Content for in-call audio-only calls. + * Audio call is a call which returns `false` for [Call.hasCapability] when the argument is `SendVideo` + * + * @param call the call. + */ + @Composable + public fun StreamCallActivity.AudioCallContent(call: Call) + + /** + * Content for in-call for every other call that has video capabilities. + * Default call is a call which returns `true` for [Call.hasCapability] when the argument is `SendVideo` + * + * @param call the call. + */ + @Composable + public fun StreamCallActivity.VideoCallContent(call: Call) + + /** + * Content for outgoing call. + * + * @param call the call. + * @param modifier the UI modifier + * @param isVideoType if the call is video or not + * @param isShowingHeader if the header will be shown + * @param headerContent the content of the header + * @param detailsContent the details content (participant avatars etc..) + */ + @Composable + public fun StreamCallActivity.OutgoingCallContent( + modifier: Modifier, + call: Call, + isVideoType: Boolean, + isShowingHeader: Boolean, + headerContent: (@Composable ColumnScope.() -> Unit)?, + detailsContent: ( + @Composable ColumnScope.( + participants: List, + topPadding: Dp, + ) -> Unit + )?, + controlsContent: (@Composable BoxScope.() -> Unit)?, + onBackPressed: () -> Unit, + onCallAction: (CallAction) -> Unit, + ) + + /** + * Content for incoming call. + * + * @param call the call. + * @param modifier the modifier + * @param isVe + */ + @Composable + public fun StreamCallActivity.IncomingCallContent( + modifier: Modifier, + call: Call, + isVideoType: Boolean, + isShowingHeader: Boolean, + headerContent: (@Composable ColumnScope.() -> Unit)?, + detailsContent: ( + @Composable ColumnScope.( + participants: List, + topPadding: Dp, + ) -> Unit + )?, + controlsContent: (@Composable BoxScope.() -> Unit)?, + onBackPressed: () -> Unit, + onCallAction: (CallAction) -> Unit, + ) + + /** + * Content for when the call was not answered. + * + * @param call the call. + */ + @Composable + public fun StreamCallActivity.NoAnswerContent(call: Call) + + /** + * Call when the call was rejected. + * + * @param call the call. + */ + @Composable + public fun StreamCallActivity.RejectedContent(call: Call) + + /** + * Content shown when the required permissions are not granted and the call cannot happen. + * Note: There are other places that permissions are required like in the service etc.. + * Best practice is to request these permissions a screen before starting the call. + */ + @Composable + public fun StreamCallActivity.PermissionsRationaleContent( + call: Call, + granted: List, + notGranted: List, + showRationale: Boolean, + ) +} diff --git a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/RingingCallContent.kt b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/RingingCallContent.kt index c78b9bf8e6..c586a2ce0f 100644 --- a/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/RingingCallContent.kt +++ b/stream-video-android-ui-compose/src/main/kotlin/io/getstream/video/android/compose/ui/components/call/ringing/RingingCallContent.kt @@ -110,6 +110,7 @@ public fun RingingCallContent( onCallAction: (CallAction) -> Unit, ) -> Unit )? = null, + onIdle: @Composable () -> Unit = {}, ) { val ringingState by call.state.ringingState.collectAsStateWithLifecycle() @@ -174,12 +175,9 @@ public fun RingingCallContent( onAcceptedContent.invoke() } - RingingState.Idle -> { - // Call state is not ready yet? Show loading? - } - else -> { - // Unknown + // Includes Idle + onIdle.invoke() } } } diff --git a/stream-video-android-ui-core/api/stream-video-android-ui-core.api b/stream-video-android-ui-core/api/stream-video-android-ui-core.api index 56677a886b..0778369813 100644 --- a/stream-video-android-ui-core/api/stream-video-android-ui-core.api +++ b/stream-video-android-ui-core/api/stream-video-android-ui-core.api @@ -40,6 +40,8 @@ public abstract class io/getstream/video/android/ui/common/StreamCallActivity : public final fun enterPictureInPicture ()V public fun get (Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V public static synthetic fun get$default (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V + public fun getConfiguration ()Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; + public abstract fun getUiDelegate ()Lio/getstream/video/android/ui/common/StreamActivityUiDelegate; public fun isVideoCall (Lio/getstream/video/android/core/Call;)Z public fun join (Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V public static synthetic fun join$default (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V @@ -48,9 +50,12 @@ public abstract class io/getstream/video/android/ui/common/StreamCallActivity : public fun onBackPressed (Lio/getstream/video/android/core/Call;)V public fun onCallAction (Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/call/state/CallAction;)V public fun onCallEvent (Lio/getstream/video/android/core/Call;Lorg/openapitools/client/models/VideoEvent;)V + public fun onConnectionEvent (Lio/getstream/video/android/core/Call;Lio/getstream/video/android/core/RealtimeConnection;)V public final fun onCreate (Landroid/os/Bundle;)V public final fun onCreate (Landroid/os/Bundle;Landroid/os/PersistableBundle;)V public fun onCreate (Landroid/os/Bundle;Landroid/os/PersistableBundle;Lio/getstream/video/android/core/Call;)V + public fun onEnded (Lio/getstream/video/android/core/Call;)V + public fun onFailed (Ljava/lang/Exception;)V public fun onIntentAction (Lio/getstream/video/android/core/Call;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V public static synthetic fun onIntentAction$default (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V public fun onLastParticipant (Lio/getstream/video/android/core/Call;)V @@ -64,12 +69,33 @@ public abstract class io/getstream/video/android/ui/common/StreamCallActivity : public fun onStop (Lio/getstream/video/android/core/Call;)V public fun reject (Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V public static synthetic fun reject$default (Lio/getstream/video/android/ui/common/StreamCallActivity;Lio/getstream/video/android/core/Call;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V - public abstract fun uiDelegate ()Lio/getstream/video/android/ui/common/StreamActivityUiDelegate; } public final class io/getstream/video/android/ui/common/StreamCallActivity$Companion { - public final fun callIntent (Landroid/content/Context;Lio/getstream/video/android/model/StreamCallId;Ljava/util/List;ZLjava/lang/String;Ljava/lang/Class;)Landroid/content/Intent; - public static synthetic fun callIntent$default (Lio/getstream/video/android/ui/common/StreamCallActivity$Companion;Landroid/content/Context;Lio/getstream/video/android/model/StreamCallId;Ljava/util/List;ZLjava/lang/String;Ljava/lang/Class;ILjava/lang/Object;)Landroid/content/Intent; + public final fun callIntent (Landroid/content/Context;Lio/getstream/video/android/model/StreamCallId;Ljava/util/List;ZLjava/lang/String;Ljava/lang/Class;Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration;)Landroid/content/Intent; + public static synthetic fun callIntent$default (Lio/getstream/video/android/ui/common/StreamCallActivity$Companion;Landroid/content/Context;Lio/getstream/video/android/model/StreamCallId;Ljava/util/List;ZLjava/lang/String;Ljava/lang/Class;Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration;ILjava/lang/Object;)Landroid/content/Intent; +} + +public final class io/getstream/video/android/ui/common/StreamCallActivityConfiguration { + public fun ()V + public fun (ZZLandroid/os/Bundle;)V + public synthetic fun (ZZLandroid/os/Bundle;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Z + public final fun component2 ()Z + public final fun component3 ()Landroid/os/Bundle; + public final fun copy (ZZLandroid/os/Bundle;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; + public static synthetic fun copy$default (Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration;ZZLandroid/os/Bundle;ILjava/lang/Object;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; + public fun equals (Ljava/lang/Object;)Z + public final fun getCloseScreenOnCallEnded ()Z + public final fun getCloseScreenOnError ()Z + public final fun getCustom ()Landroid/os/Bundle; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/getstream/video/android/ui/common/StreamCallActivityConfigurationKt { + public static final fun extractStreamActivityConfig (Landroid/os/Bundle;)Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration; + public static final fun toBundle (Lio/getstream/video/android/ui/common/StreamCallActivityConfiguration;)Landroid/os/Bundle; } public final class io/getstream/video/android/ui/common/databinding/StreamVideoContentParticipantBinding : androidx/viewbinding/ViewBinding { diff --git a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt index 97b29921f6..6d2d3357c9 100644 --- a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt +++ b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivity.kt @@ -34,6 +34,7 @@ import io.getstream.result.onErrorSuspend import io.getstream.result.onSuccessSuspend import io.getstream.video.android.core.Call import io.getstream.video.android.core.EventSubscription +import io.getstream.video.android.core.RealtimeConnection import io.getstream.video.android.core.StreamVideo import io.getstream.video.android.core.call.RtcSession import io.getstream.video.android.core.call.state.AcceptCall @@ -51,6 +52,8 @@ import io.getstream.video.android.model.StreamCallId import io.getstream.video.android.model.streamCallId import io.getstream.video.android.ui.common.util.StreamCallActivityDelicateApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.openapitools.client.models.CallEndedEvent @@ -87,12 +90,16 @@ public abstract class StreamCallActivity : ComponentActivity() { leaveWhenLastInCall: Boolean = DEFAULT_LEAVE_WHEN_LAST, action: String? = null, clazz: Class, + configuration: StreamCallActivityConfiguration = StreamCallActivityConfiguration(), ): Intent { return Intent(context, clazz).apply { // Setup the outgoing call action action?.let { this.action = it } + val config = configuration.toBundle() + // Add the config + putExtra(StreamCallActivityConfigStrings.EXTRA_STREAM_CONFIG, config) // Add the generated call ID and other params putExtra(NotificationHandler.INTENT_EXTRA_CALL_CID, cid) putExtra(EXTRA_LEAVE_WHEN_LAST, leaveWhenLastInCall) @@ -107,22 +114,64 @@ public abstract class StreamCallActivity : ComponentActivity() { } // Internal state - private var subscription: EventSubscription? = null + private var callEventSubscription: EventSubscription? = null + private var callSocketConnectionMonitor: Job? = null private lateinit var cachedCall: Call - private val onSuccessFinish: suspend (Call) -> Unit = { + private lateinit var config: StreamCallActivityConfiguration + private lateinit var delegate: StreamActivityUiDelegate + private val onSuccessFinish: suspend (Call) -> Unit = { call -> logger.w { "The call was successfully finished! Closing activity" } - finish() + onEnded(call) + if (configuration.closeScreenOnCallEnded) { + finish() + } } - private val onErrorFinish: suspend (Exception) -> Unit = { - logger.e(it) { "Something went wrong, finishing the activity!" } - finish() + private val onErrorFinish: suspend (Exception) -> Unit = { error -> + logger.e(error) { "Something went wrong, finishing the activity!" } + onFailed(error) + if (configuration.closeScreenOnError) { + finish() + } } + // Public values + /** + * Each activity needs its onw ui delegate that will set content to the activity. + * + * Any extending activity must provide its own ui delegate that manages the UI. + */ + public abstract val uiDelegate: StreamActivityUiDelegate + + /** + * A configuration object returned for this activity controlling various behaviors of the activity. + * Can be overridden for further control on the behavior. + * + * This is delicate API because it loads the config from the extra, you can pass the + * configuration object in the [callIntent] method and it will be correctly passed here. + * You can override it and return a custom configuration all the time, in which case + * the configuration passed in [callIntent] is ignored. + */ + @StreamCallActivityDelicateApi + public open val configuration: StreamCallActivityConfiguration + get() { + if (!::config.isInitialized) { + try { + val bundledConfig = + intent.getBundleExtra(StreamCallActivityConfigStrings.EXTRA_STREAM_CONFIG) + config = + bundledConfig?.extractStreamActivityConfig() + ?: StreamCallActivityConfiguration() + } catch (e: Exception) { + config = StreamCallActivityConfiguration() + logger.e(e) { "Failed to load config using default!" } + } + } + return config + } + // Platform restriction public final override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val uiDelegate = uiDelegate() - uiDelegate.loadingContent(this) onPreCreate(savedInstanceState, null) logger.d { "Entered [onCreate(Bundle?)" } initializeCallOrFail( @@ -131,11 +180,11 @@ public abstract class StreamCallActivity : ComponentActivity() { onSuccess = { instanceState, persistentState, call, action -> logger.d { "Calling [onCreate(Call)], because call is initialized $call" } onCreate(instanceState, persistentState, call) - onIntentAction(call, action, onError = { - finish() - }) + onIntentAction(call, action, onError = onErrorFinish) }, onError = { + // We are not calling onErrorFinish here on purpose + // we want to crash if we cannot initialize the call logger.e(it) { "Failed to initialize call." } throw it }, @@ -147,8 +196,6 @@ public abstract class StreamCallActivity : ComponentActivity() { persistentState: PersistableBundle?, ) { super.onCreate(savedInstanceState) - val uiDelegate = uiDelegate() - uiDelegate.loadingContent(this) onPreCreate(savedInstanceState, persistentState) logger.d { "Entered [onCreate(Bundle, PersistableBundle?)" } initializeCallOrFail( @@ -157,11 +204,11 @@ public abstract class StreamCallActivity : ComponentActivity() { onSuccess = { instanceState, persistedState, call, action -> logger.d { "Calling [onCreate(Call)], because call is initialized $call" } onCreate(instanceState, persistedState, call) - onIntentAction(call, action, onError = { - finish() - }) + onIntentAction(call, action, onError = onErrorFinish) }, onError = { + // We are not calling onErrorFinish here on purpose + // we want to crash if we cannot initialize the call logger.e(it) { "Failed to initialize call." } throw it }, @@ -252,11 +299,16 @@ public abstract class StreamCallActivity : ComponentActivity() { } /** - * Called when the activity is created, but the SDK is not yet initialized and the call is not retrieved. - * Can be used to show loading progress bar or some loading gradient instead of white screen. + * Called when the activity is created, but the SDK is not yet initialized. + * Initializes the configuration and UI delegates. */ + @CallSuper + @StreamCallActivityDelicateApi public open fun onPreCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { - logger.d { "Set pre-init content." } + logger.d { "Pre-create" } + val config = configuration // Called before the delegate + logger.d { "Activity pre-created with configuration [$config]" } + uiDelegate.loadingContent(this) } /** @@ -271,16 +323,9 @@ public abstract class StreamCallActivity : ComponentActivity() { call: Call, ) { logger.d { "[onCreate(Bundle,PersistableBundle,Call)] setting up compose delegate." } - val uiDelegate = uiDelegate() uiDelegate.setContent(this, call) } - /** - * Returns a delegate that uses compose for its UI. - * - */ - public abstract fun uiDelegate(): StreamActivityUiDelegate - /** * Called when the activity is resumed. Makes sure the call object is available. * @@ -303,6 +348,23 @@ public abstract class StreamCallActivity : ComponentActivity() { logger.d { "DefaultCallActivity - Paused (call -> $call)" } } + /** + * Called when the activity has failed for any reason. + * The activity will finish after this call, to prevent the `finish()` provide a different [StreamCallActivityConfiguration]. + */ + public open fun onFailed(exception: Exception) { + // No - op + } + + /** + * Called when call has ended successfully, either by action from the user or + * backend event. The activity will finish after this call. + * To prevent it, provide a different [StreamCallActivityConfiguration] + */ + public open fun onEnded(call: Call) { + // No-op + } + /** * Called when the activity is stopped. Makes sure the call object is available. * Will leave the call if [onStop] is called while in Picture-in-picture mode. @@ -328,7 +390,8 @@ public abstract class StreamCallActivity : ComponentActivity() { // Decision making @StreamCallActivityDelicateApi - public open fun isVideoCall(call: Call): Boolean = call.hasCapability(OwnCapability.SendVideo) + public open fun isVideoCall(call: Call): Boolean = + call.hasCapability(OwnCapability.SendVideo) // Picture in picture (for Video calls) /** @@ -561,6 +624,8 @@ public abstract class StreamCallActivity : ComponentActivity() { @CallSuper public open fun onCallAction(call: Call, action: CallAction) { logger.d { "======-- Action --======\n$action\n================" } + val onSuccess = { + } when (action) { is LeaveCall -> { leave(call, onSuccessFinish, onErrorFinish) @@ -609,11 +674,7 @@ public abstract class StreamCallActivity : ComponentActivity() { when (event) { is CallEndedEvent -> { // In any case finish the activity, the call is done for - leave(call, onSuccess = { - finish() - }, onError = { - finish() - }) + leave(call, onSuccess = onSuccessFinish, onError = onErrorFinish) } is ParticipantLeftEvent, is CallSessionParticipantLeftEvent -> { @@ -651,6 +712,35 @@ public abstract class StreamCallActivity : ComponentActivity() { // No-op by default } + /** + * Called when there has been a new event from the socket. + * + * @param call the call + * @param state the state + */ + @CallSuper + @StreamCallActivityDelicateApi + public open fun onConnectionEvent(call: Call, state: RealtimeConnection) { + when (state) { + RealtimeConnection.Disconnected -> { + lifecycleScope.launch { + onSuccessFinish.invoke(call) + } + } + is RealtimeConnection.Failed -> { + lifecycleScope.launch { + val conn = state as? RealtimeConnection.Failed + val throwable = Exception("${conn?.error}") + logger.e(throwable) { "Call connection failed." } + onErrorFinish.invoke(throwable) + } + } + else -> { + // No-op + } + } + } + // Internal logic private fun initializeCallOrFail( savedInstanceState: Bundle?, @@ -676,10 +766,15 @@ public abstract class StreamCallActivity : ComponentActivity() { cid, onSuccess = { call -> cachedCall = call - subscription?.dispose() - subscription = cachedCall.subscribe { event -> + callEventSubscription?.dispose() + callEventSubscription = cachedCall.subscribe { event -> onCallEvent(cachedCall, event) } + callSocketConnectionMonitor = lifecycleScope.launch(Dispatchers.IO) { + cachedCall.state.connection.collectLatest { + onConnectionEvent(call, it) + } + } onSuccess?.invoke( savedInstanceState, persistentState, diff --git a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt new file mode 100644 index 0000000000..95dfc6bb6f --- /dev/null +++ b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/StreamCallActivityConfiguration.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2014-2024 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.ui.common + +import android.os.Bundle + +internal object StreamCallActivityConfigStrings { + const val EXTRA_STREAM_CONFIG = "stream-activity-config" + const val EXTRA_CLOSE_ON_ERROR = "close-on-error" + const val EXTRA_CLOSE_ON_ENDED = "close-on-ended" + const val EXTRA_CUSTOM = "custom-fields" +} + +/** + * A configuration that controls various behaviors of the [StreamCallActivity]. + */ +public data class StreamCallActivityConfiguration( + /** When there has been a technical error, close the screen. */ + val closeScreenOnError: Boolean = true, + /** When the call has ended for any reason, close the screen */ + val closeScreenOnCallEnded: Boolean = true, + /** + * Custom configuration extension for any extending classes. + * Can be used same as normal extras. + */ + val custom: Bundle? = null, +) + +/** + * Extract a [StreamCallActivityConfigStrings] from bundle. + */ +public fun Bundle.extractStreamActivityConfig(): StreamCallActivityConfiguration { + val closeScreenOnError = + getBoolean(StreamCallActivityConfigStrings.EXTRA_CLOSE_ON_ERROR, true) + val closeScreenOnCallEnded = + getBoolean(StreamCallActivityConfigStrings.EXTRA_CLOSE_ON_ENDED, true) + val custom = getBundle(StreamCallActivityConfigStrings.EXTRA_CUSTOM) + return StreamCallActivityConfiguration( + closeScreenOnError = closeScreenOnError, + closeScreenOnCallEnded = closeScreenOnCallEnded, + custom = custom, + ) +} + +/** + * Add a [StreamCallActivityConfiguration] into a bundle. + */ +public fun StreamCallActivityConfiguration.toBundle(): Bundle { + val bundle = Bundle() + bundle.putBoolean(StreamCallActivityConfigStrings.EXTRA_CLOSE_ON_ERROR, closeScreenOnError) + bundle.putBoolean(StreamCallActivityConfigStrings.EXTRA_CLOSE_ON_ENDED, closeScreenOnCallEnded) + bundle.putBundle(StreamCallActivityConfigStrings.EXTRA_CUSTOM, custom) + return bundle +} diff --git a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/util/StreamCallActivityDelicateApi.kt b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/util/StreamCallActivityDelicateApi.kt index 96e7e8d3e5..d3f5e3310d 100644 --- a/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/util/StreamCallActivityDelicateApi.kt +++ b/stream-video-android-ui-core/src/main/kotlin/io/getstream/video/android/ui/common/util/StreamCallActivityDelicateApi.kt @@ -21,7 +21,9 @@ package io.getstream.video.android.ui.common.util @Target( AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.PROPERTY, ) @RequiresOptIn( level = RequiresOptIn.Level.WARNING,