Skip to content
Draft
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
47 changes: 32 additions & 15 deletions stream-video-android-core/api/stream-video-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -8886,7 +8886,6 @@ public final class io/getstream/video/android/core/ClientState {
public final fun getRingingCall ()Lkotlinx/coroutines/flow/StateFlow;
public final fun getTelecomIntegrationType ()Lio/getstream/video/android/core/notifications/internal/telecom/TelecomIntegrationType;
public final fun getUser ()Lkotlinx/coroutines/flow/StateFlow;
public final fun handleError (Lio/getstream/result/Error;)V
public final fun handleEvent (Lio/getstream/android/video/generated/models/VideoEvent;)V
public final fun hasActiveOrRingingCall ()Z
public final fun removeActiveCall ()V
Expand All @@ -8899,40 +8898,58 @@ public abstract interface class io/getstream/video/android/core/ConnectionState
}

public final class io/getstream/video/android/core/ConnectionState$Connected : io/getstream/video/android/core/ConnectionState {
public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$Connected;
public fun <init> (Lio/getstream/android/core/api/model/connection/StreamConnectedUser;Ljava/lang/String;)V
public final fun component1 ()Lio/getstream/android/core/api/model/connection/StreamConnectedUser;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Lio/getstream/android/core/api/model/connection/StreamConnectedUser;Ljava/lang/String;)Lio/getstream/video/android/core/ConnectionState$Connected;
public static synthetic fun copy$default (Lio/getstream/video/android/core/ConnectionState$Connected;Lio/getstream/android/core/api/model/connection/StreamConnectedUser;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/video/android/core/ConnectionState$Connected;
public fun equals (Ljava/lang/Object;)Z
public final fun getConnectedUser ()Lio/getstream/android/core/api/model/connection/StreamConnectedUser;
public final fun getConnectionId ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/ConnectionState$Disconnected : io/getstream/video/android/core/ConnectionState {
public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$Disconnected;
public abstract interface class io/getstream/video/android/core/ConnectionState$Connecting : io/getstream/video/android/core/ConnectionState {
}

public final class io/getstream/video/android/core/ConnectionState$Connecting$Authenticating : io/getstream/video/android/core/ConnectionState$Connecting {
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lio/getstream/video/android/core/ConnectionState$Connecting$Authenticating;
public static synthetic fun copy$default (Lio/getstream/video/android/core/ConnectionState$Connecting$Authenticating;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/video/android/core/ConnectionState$Connecting$Authenticating;
public fun equals (Ljava/lang/Object;)Z
public final fun getUserId ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/ConnectionState$Failed : io/getstream/video/android/core/ConnectionState {
public fun <init> (Lio/getstream/result/Error;)V
public final fun getError ()Lio/getstream/result/Error;
}

public final class io/getstream/video/android/core/ConnectionState$Loading : io/getstream/video/android/core/ConnectionState {
public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$Loading;
public final class io/getstream/video/android/core/ConnectionState$Connecting$Opening : io/getstream/video/android/core/ConnectionState$Connecting {
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lio/getstream/video/android/core/ConnectionState$Connecting$Opening;
public static synthetic fun copy$default (Lio/getstream/video/android/core/ConnectionState$Connecting$Opening;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/video/android/core/ConnectionState$Connecting$Opening;
public fun equals (Ljava/lang/Object;)Z
public final fun getUserId ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/ConnectionState$PreConnect : io/getstream/video/android/core/ConnectionState {
public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$PreConnect;
public final class io/getstream/video/android/core/ConnectionState$Disconnected : io/getstream/video/android/core/ConnectionState {
public fun <init> ()V
public fun <init> (Ljava/lang/Throwable;)V
public synthetic fun <init> (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/Throwable;
public final fun copy (Ljava/lang/Throwable;)Lio/getstream/video/android/core/ConnectionState$Disconnected;
public static synthetic fun copy$default (Lio/getstream/video/android/core/ConnectionState$Disconnected;Ljava/lang/Throwable;ILjava/lang/Object;)Lio/getstream/video/android/core/ConnectionState$Disconnected;
public fun equals (Ljava/lang/Object;)Z
public final fun getCause ()Ljava/lang/Throwable;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/getstream/video/android/core/ConnectionState$Reconnecting : io/getstream/video/android/core/ConnectionState {
public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$Reconnecting;
public final class io/getstream/video/android/core/ConnectionState$Idle : io/getstream/video/android/core/ConnectionState {
public static final field INSTANCE Lio/getstream/video/android/core/ConnectionState$Idle;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,9 @@ class AndroidDeviceTest : IntegrationTestBase(connectCoordinatorWS = false) {

@Test
fun coordinatorWSConnection() = runTest {
assertThat(client.state.connection.value).isEqualTo(ConnectionState.Connected)
assertThat(
client.state.connection.value,
).isInstanceOf(ConnectionState.Connected::class.java)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,38 @@
package io.getstream.video.android.core

import androidx.compose.runtime.Stable
import io.getstream.android.core.api.model.connection.StreamConnectedUser
import io.getstream.android.core.api.model.connection.StreamConnectionState
import io.getstream.android.video.generated.models.CallCreatedEvent
import io.getstream.android.video.generated.models.CallRingEvent
import io.getstream.android.video.generated.models.ConnectedEvent
import io.getstream.android.video.generated.models.VideoEvent
import io.getstream.log.taggedLogger
import io.getstream.result.Error
import io.getstream.video.android.core.internal.InternalStreamVideoApi
import io.getstream.video.android.core.notifications.internal.service.CallService
import io.getstream.video.android.core.notifications.internal.service.ServiceIntentBuilder
import io.getstream.video.android.core.notifications.internal.service.ServiceLauncher
import io.getstream.video.android.core.notifications.internal.telecom.TelecomIntegrationType
import io.getstream.video.android.core.socket.coordinator.state.VideoSocketState
import io.getstream.video.android.core.socket.coordinator.v2.toVideoConnectionState
import io.getstream.video.android.core.utils.safeCallWithDefault
import io.getstream.video.android.model.User
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

// These are UI states, need to move out.
// Public coordinator connection state — mirrors core's StreamConnectionState 1:1.
@Stable
public sealed interface ConnectionState {
public data object PreConnect : ConnectionState
public data object Loading : ConnectionState
public data object Connected : ConnectionState
public data object Reconnecting : ConnectionState
public data object Disconnected : ConnectionState
public class Failed(val error: Error) : ConnectionState
public data object Idle : ConnectionState
public data class Disconnected(val cause: Throwable? = null) : ConnectionState
public data class Connected(
val connectedUser: StreamConnectedUser,
val connectionId: String,
) : ConnectionState
public sealed interface Connecting : ConnectionState {
public data class Opening(val userId: String) : Connecting
public data class Authenticating(val userId: String) : Connecting
}
}

// These are UI states, need to move out.
Expand All @@ -65,7 +69,7 @@ class ClientState(private val client: StreamVideo) {
// Internal data
private val _user: MutableStateFlow<User?> = MutableStateFlow(client.user)
private val _connection: MutableStateFlow<ConnectionState> =
MutableStateFlow(ConnectionState.PreConnect)
MutableStateFlow(ConnectionState.Idle)
internal val _ringingCall: MutableStateFlow<Call?> = MutableStateFlow(null)
private val _activeCall: MutableStateFlow<Call?> = MutableStateFlow(null)

Expand Down Expand Up @@ -127,12 +131,7 @@ class ClientState(private val client: StreamVideo) {
* Most event logic happens in the Call instead of the client
*/
fun handleEvent(event: VideoEvent) {
// mark connected
when (event) {
is ConnectedEvent -> {
_connection.value = ConnectionState.Connected
}

is CallCreatedEvent -> {
// what's the right thing to do here?
// if it's ringing we add it
Expand All @@ -150,28 +149,8 @@ class ClientState(private val client: StreamVideo) {
}
}

internal fun handleState(socketState: VideoSocketState) {
val state = when (socketState) {
// Before connection is established
is VideoSocketState.Disconnected.Stopped -> ConnectionState.PreConnect
// Loading
is VideoSocketState.Connecting -> ConnectionState.Loading
// Connected
is VideoSocketState.Connected -> ConnectionState.Connected
// Reconnecting
is VideoSocketState.Disconnected.DisconnectedTemporarily -> ConnectionState.Reconnecting
is VideoSocketState.RestartConnection -> ConnectionState.Reconnecting
// Disconnected
is VideoSocketState.Disconnected.WebSocketEventLost -> ConnectionState.Disconnected
is VideoSocketState.Disconnected.NetworkDisconnected -> ConnectionState.Disconnected
is VideoSocketState.Disconnected.DisconnectedByRequest -> ConnectionState.Disconnected
is VideoSocketState.Disconnected.DisconnectedPermanently -> ConnectionState.Disconnected
}
_connection.value = state
}

fun handleError(error: Error) {
_connection.value = ConnectionState.Failed(error)
internal fun handleStreamState(streamState: StreamConnectionState) {
_connection.value = streamState.toVideoConnectionState()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,7 @@ internal class StreamVideoClient internal constructor(
}

override fun onState(state: StreamConnectionState) {
// TODO(02-03): replace with this@StreamVideoClient.state.handleStreamState(state)
// once ClientState.handleStreamState lands in Plan 02-03. Held to validate the
// listener wiring at SDK init; connection-state routing is deferred to the next plan.
this@StreamVideoClient.state.handleStreamState(state)
}

override fun onError(err: Throwable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,24 @@
package io.getstream.video.android.core.socket.coordinator.v2

import io.getstream.android.core.api.model.connection.StreamConnectionState
import io.getstream.result.Error
import io.getstream.video.android.core.ConnectionState

/**
* Maps a core [StreamConnectionState] onto the SDK's existing [ConnectionState] shape.
* Maps a core [StreamConnectionState] onto the SDK's [ConnectionState] shape 1:1.
*
* The mapping produced here is a placeholder that preserves compilation until a later plan
* reshapes the video [ConnectionState] sealed interface to mirror the richer core states 1:1
* (D-04, D-09). Once that reshape lands, both this function body and the collaborating tests
* flip together in a single commit.
* Both hierarchies expose the same five variants (Idle, Connecting.Opening,
* Connecting.Authenticating, Connected, Disconnected); this extension carries the payloads
* across the boundary so integrators observe a video-owned type while the underlying transitions
* are driven by the core client.
*/
internal fun StreamConnectionState.toVideoConnectionState(): ConnectionState {
// TODO(02-03): replace placeholder mapping when ConnectionState sealed interface is
// rewritten per D-04/D-09.
return when (this) {
is StreamConnectionState.Idle -> ConnectionState.PreConnect
is StreamConnectionState.Connecting.Opening -> ConnectionState.Loading
is StreamConnectionState.Connecting.Authenticating -> ConnectionState.Loading
is StreamConnectionState.Connected -> ConnectionState.Connected
is StreamConnectionState.Disconnected -> {
val throwable = cause
if (throwable == null) {
ConnectionState.Disconnected
} else {
ConnectionState.Failed(
Error.ThrowableError(
message = throwable.message ?: "",
cause = throwable,
),
)
}
}
internal fun StreamConnectionState.toVideoConnectionState(): ConnectionState =
when (this) {
is StreamConnectionState.Idle -> ConnectionState.Idle
is StreamConnectionState.Connecting.Opening ->
ConnectionState.Connecting.Opening(userId)
is StreamConnectionState.Connecting.Authenticating ->
ConnectionState.Connecting.Authenticating(userId)
is StreamConnectionState.Connected ->
ConnectionState.Connected(connectedUser, connectionId)
is StreamConnectionState.Disconnected -> ConnectionState.Disconnected(cause)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,16 @@ class ClientAndAuthTest : TestBase() {
user = testData.users["thierry"]!!,
token = authData!!.token,
).build()
assertThat(client.state.connection.value).isEqualTo(ConnectionState.PreConnect)
assertThat(client.state.connection.value).isEqualTo(ConnectionState.Idle)
val clientImpl = client as StreamVideoClient

val connectResultDeferred = clientImpl.connectAsync()

val connectResult = connectResultDeferred.await()
delay(100L)
assertThat(client.state.connection.value).isEqualTo(ConnectionState.Connected)
assertThat(
client.state.connection.value,
).isInstanceOf(ConnectionState.Connected::class.java)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,66 +22,66 @@ import io.getstream.video.android.core.socket.coordinator.v2.toVideoConnectionSt
import org.junit.Test
import java.util.Date
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue

/**
* Table-driven mapping test for [StreamConnectionState] -> [ConnectionState] extension.
*
* Asserts the temporary placeholder mapping shipped by Plan 02-01. A subsequent plan reshapes
* the video [ConnectionState] sealed interface to mirror core 1:1; both this test and the
* mapper body flip together in that plan.
* Asserts the final 1:1 mapping: each core variant produces the video-owned counterpart with
* payload fields carried through verbatim (userId, connectedUser + connectionId, cause).
*/
internal class ClientStateConnectionMappingTest {

@Test
fun `Idle maps to PreConnect`() {
fun `Idle maps to Idle`() {
val result = (StreamConnectionState.Idle as StreamConnectionState).toVideoConnectionState()
assertEquals(ConnectionState.PreConnect, result)
// TODO(02-03): update expected value when ConnectionState sealed interface is rewritten
assertEquals(ConnectionState.Idle, result)
}

@Test
fun `Connecting Opening maps to Loading`() {
fun `Connecting Opening maps to Connecting Opening with userId`() {
val result: ConnectionState =
StreamConnectionState.Connecting.Opening("user-1").toVideoConnectionState()
assertEquals(ConnectionState.Loading, result)
// TODO(02-03): update expected value when ConnectionState sealed interface is rewritten
assertTrue(result is ConnectionState.Connecting.Opening)
assertEquals("user-1", result.userId)
}

@Test
fun `Connecting Authenticating maps to Loading`() {
fun `Connecting Authenticating maps to Connecting Authenticating with userId`() {
val result: ConnectionState =
StreamConnectionState.Connecting.Authenticating("user-1").toVideoConnectionState()
assertEquals(ConnectionState.Loading, result)
// TODO(02-03): update expected value when ConnectionState sealed interface is rewritten
assertTrue(result is ConnectionState.Connecting.Authenticating)
assertEquals("user-1", result.userId)
}

@Test
fun `Connected maps to Connected`() {
fun `Connected maps to Connected with connectedUser and connectionId`() {
val fixture = connectedUserFixture()
val result: ConnectionState = StreamConnectionState
.Connected(connectedUserFixture(), "connection-id")
.Connected(fixture, "connection-id")
.toVideoConnectionState()
assertEquals(ConnectionState.Connected, result)
// TODO(02-03): update expected value when ConnectionState sealed interface is rewritten
assertTrue(result is ConnectionState.Connected)
assertSame(fixture, result.connectedUser)
assertEquals("connection-id", result.connectionId)
}

@Test
fun `Disconnected with null cause maps to Disconnected`() {
fun `Disconnected with null cause maps to Disconnected with null cause`() {
val result: ConnectionState =
StreamConnectionState.Disconnected(cause = null).toVideoConnectionState()
assertEquals(ConnectionState.Disconnected, result)
// TODO(02-03): update expected value when ConnectionState sealed interface is rewritten
assertTrue(result is ConnectionState.Disconnected)
assertNull(result.cause)
}

@Test
fun `Disconnected with cause maps to Failed`() {
fun `Disconnected with cause maps to Disconnected with same cause`() {
val boom = IllegalStateException("boom")
val result: ConnectionState =
StreamConnectionState.Disconnected(cause = boom).toVideoConnectionState()

assertTrue(result is ConnectionState.Failed)
assertEquals("boom", result.error.message)
// TODO(02-03): update expected value when ConnectionState sealed interface is rewritten
assertTrue(result is ConnectionState.Disconnected)
assertSame(boom, result.cause)
}

private fun connectedUserFixture(): StreamConnectedUser = StreamConnectedUser(
Expand Down
Loading
Loading