From a4786a9ad24610618b55524fa2396a8771e5e8f8 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:05:20 +0300 Subject: [PATCH 1/4] Refactor unknown event handling --- .../android/core/socket/CoordinatorSocket.kt | 71 +++++++++++-------- .../openapitools/client/models/VideoEvent.kt | 16 +++-- 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt index e6dccccf9a..2a196825e8 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt @@ -29,6 +29,7 @@ import okhttp3.WebSocket import org.openapitools.client.infrastructure.Serializer import org.openapitools.client.models.ConnectUserDetailsRequest import org.openapitools.client.models.ConnectedEvent +import org.openapitools.client.models.UnknownVideoEvent import org.openapitools.client.models.VideoEvent import org.openapitools.client.models.WSAuthMessageRequest @@ -81,45 +82,59 @@ public class CoordinatorSocket( override fun onMessage(webSocket: WebSocket, text: String) { logger.d { "[onMessage] text: $text " } + if (text.isEmpty()) { + logger.w { "[onMessage] Received empty socket message" } + return + } + scope.launch(singleThreadDispatcher) { // parse the message - val jsonAdapter: JsonAdapter = - Serializer.moshi.adapter(VideoEvent::class.java) - val processedEvent = try { + val jsonAdapter: JsonAdapter = Serializer.moshi.adapter( + VideoEvent::class.java, + ) + + val parsedEvent = try { jsonAdapter.fromJson(text) } catch (e: Throwable) { logger.w { "[onMessage] VideoEvent parsing error ${e.message}" } null } - if (processedEvent is ConnectedEvent) { - _connectionId.value = processedEvent.connectionId - setConnectedStateAndContinue(processedEvent) + when (parsedEvent) { + is UnknownVideoEvent -> + logger.w { "[onMessage] Received unknown VideoEvent type: ${parsedEvent.expectedType}" } + + null -> tryParseApiError(text) + + else -> processEvent(parsedEvent) + } + } + } + + private fun tryParseApiError(text: String) { + try { + val json = Json { + prettyPrint = true + ignoreUnknownKeys = true } - // handle errors - if (text.isNotEmpty() && processedEvent == null) { - try { - val parsedError = Json.decodeFromString(text) - parsedError.let { - logger.w { "[onMessage] socketErrorEvent: ${parsedError.error}" } - handleError(it.error) - } - } catch (e: Throwable) { - logger.w { "[onMessage] socketErrorEvent parsing error: ${e.message}" } - handleError(e) - } - } else { - logger.d { "parsed event $processedEvent" } - - // emit the message - if (processedEvent == null) { - logger.w { "[onMessage] failed to parse event: $text" } - } else { - ackHealthMonitor() - events.emit(processedEvent) - } + val parsedError = json.decodeFromString(text) + parsedError.let { + logger.w { "[onMessage] SocketError: ${parsedError.error}" } + handleError(it.error) } + } catch (e: Throwable) { + logger.w { "[onMessage] Error when trying to parse SocketError: ${e.message}" } + handleError(e) } } + private suspend fun processEvent(parsedEvent: VideoEvent) { + if (parsedEvent is ConnectedEvent) { + _connectionId.value = parsedEvent.connectionId + setConnectedStateAndContinue(parsedEvent) + } + + ackHealthMonitor() + events.emit(parsedEvent) + } } diff --git a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt index b5d40b00ba..26ef962f78 100644 --- a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt +++ b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt @@ -144,13 +144,13 @@ class VideoEventAdapter : JsonAdapter() { "call.session_participant_joined" -> CallSessionParticipantJoinedEvent::class.java "call.session_participant_left" -> CallSessionParticipantLeftEvent::class.java "call.session_started" -> CallSessionStartedEvent::class.java - "call.transcription_failed" -> CallTranscriptionFailedEvent::class.java - "call.transcription_ready" -> CallTranscriptionReadyEvent::class.java - "call.transcription_started" -> CallTranscriptionStartedEvent::class.java - "call.transcription_stopped" -> CallTranscriptionStoppedEvent::class.java "call.unblocked_user" -> UnblockedUserEvent::class.java "call.updated" -> CallUpdatedEvent::class.java "call.user_muted" -> CallUserMutedEvent::class.java + "call.transcription_started" -> CallTranscriptionStartedEvent::class.java + "call.transcription_stopped" -> CallTranscriptionStoppedEvent::class.java + "call.transcription_ready" -> CallTranscriptionReadyEvent::class.java + "call.transcription_failed" -> CallTranscriptionFailedEvent::class.java "connection.error" -> ConnectionErrorEvent::class.java "connection.ok" -> ConnectedEvent::class.java "custom" -> CustomVideoEvent::class.java @@ -163,7 +163,13 @@ class VideoEventAdapter : JsonAdapter() { "user.reactivated" -> UserReactivatedEvent::class.java "user.unbanned" -> UserUnbannedEvent::class.java "user.updated" -> UserUpdatedEvent::class.java - else -> throw IllegalArgumentException("Unknown type: $type") + else -> UnknownVideoEvent(type)::class.java } } } + +data class UnknownVideoEvent( + @Json(name = "type") val expectedType: String +) : VideoEvent() { + override fun getEventType() = "unknown" +} From a61c4a9335b5108133eb9125362a0cc4f08a5800 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Wed, 5 Jun 2024 18:02:46 +0300 Subject: [PATCH 2/4] Use APIError model instead of SocketError --- .../android/core/socket/CoordinatorSocket.kt | 69 +++++++++---------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt index 2a196825e8..6f899dd09a 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt @@ -23,10 +23,10 @@ import io.getstream.video.android.core.internal.network.NetworkStateProvider import io.getstream.video.android.model.User import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json import okhttp3.OkHttpClient import okhttp3.WebSocket import org.openapitools.client.infrastructure.Serializer +import org.openapitools.client.models.APIError import org.openapitools.client.models.ConnectUserDetailsRequest import org.openapitools.client.models.ConnectedEvent import org.openapitools.client.models.UnknownVideoEvent @@ -82,52 +82,47 @@ public class CoordinatorSocket( override fun onMessage(webSocket: WebSocket, text: String) { logger.d { "[onMessage] text: $text " } - if (text.isEmpty()) { + if (text.isEmpty() || text == "null") { logger.w { "[onMessage] Received empty socket message" } return } scope.launch(singleThreadDispatcher) { - // parse the message - val jsonAdapter: JsonAdapter = Serializer.moshi.adapter( - VideoEvent::class.java, - ) - - val parsedEvent = try { - jsonAdapter.fromJson(text) + try { + // Try to parse an APIError first + Serializer.moshi.adapter(APIError::class.java).let { errorAdapter -> + errorAdapter.fromJson(text)?.let { parsedError -> + handleError( + ErrorResponse( + code = parsedError.code, + message = parsedError.message, + statusCode = parsedError.statusCode, + exceptionFields = parsedError.exceptionFields ?: emptyMap(), + moreInfo = parsedError.moreInfo, + ), + ) + } + } } catch (e: Throwable) { - logger.w { "[onMessage] VideoEvent parsing error ${e.message}" } - null - } - - when (parsedEvent) { - is UnknownVideoEvent -> - logger.w { "[onMessage] Received unknown VideoEvent type: ${parsedEvent.expectedType}" } - - null -> tryParseApiError(text) - - else -> processEvent(parsedEvent) + // If parsing an APIError fails, try to parse a VideoEvent + // This will also catch unmapped events, that's why we parse APIError first + try { + Serializer.moshi.adapter(VideoEvent::class.java).let { eventAdapter -> + eventAdapter.fromJson(text)?.let { parsedEvent -> + if (parsedEvent is UnknownVideoEvent) { + logger.w { "[onMessage] Received unknown VideoEvent type: ${parsedEvent.expectedType}" } + } else { + processEvent(parsedEvent) + } + } + } + } catch (e: Throwable) { + logger.w { "[onMessage] VideoEvent parsing error ${e.message}" } + } } } } - private fun tryParseApiError(text: String) { - try { - val json = Json { - prettyPrint = true - ignoreUnknownKeys = true - } - - val parsedError = json.decodeFromString(text) - parsedError.let { - logger.w { "[onMessage] SocketError: ${parsedError.error}" } - handleError(it.error) - } - } catch (e: Throwable) { - logger.w { "[onMessage] Error when trying to parse SocketError: ${e.message}" } - handleError(e) - } - } private suspend fun processEvent(parsedEvent: VideoEvent) { if (parsedEvent is ConnectedEvent) { _connectionId.value = parsedEvent.connectionId From aece3a581e0248791821cac287535bb20829e843 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:45:51 +0300 Subject: [PATCH 3/4] Use ConnectionErrorEvent model instead of APIError --- .../api/stream-video-android-core.api | 5 ++ .../android/core/socket/CoordinatorSocket.kt | 52 ++++++++----------- .../openapitools/client/models/VideoEvent.kt | 6 +-- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 44c9a8505a..b80732107c 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -10063,6 +10063,11 @@ public final class org/openapitools/client/models/UnpinResponse { public fun toString ()Ljava/lang/String; } +public final class org/openapitools/client/models/UnsupportedVideoEventException : java/lang/Exception { + public fun (Ljava/lang/String;)V + public final fun getType ()Ljava/lang/String; +} + public final class org/openapitools/client/models/UpdateCallMembersRequest { public fun ()V public fun (Ljava/util/List;Ljava/util/List;)V diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt index 6f899dd09a..f795338aac 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/socket/CoordinatorSocket.kt @@ -26,10 +26,10 @@ import kotlinx.coroutines.launch import okhttp3.OkHttpClient import okhttp3.WebSocket import org.openapitools.client.infrastructure.Serializer -import org.openapitools.client.models.APIError import org.openapitools.client.models.ConnectUserDetailsRequest import org.openapitools.client.models.ConnectedEvent -import org.openapitools.client.models.UnknownVideoEvent +import org.openapitools.client.models.ConnectionErrorEvent +import org.openapitools.client.models.UnsupportedVideoEventException import org.openapitools.client.models.VideoEvent import org.openapitools.client.models.WSAuthMessageRequest @@ -89,41 +89,35 @@ public class CoordinatorSocket( scope.launch(singleThreadDispatcher) { try { - // Try to parse an APIError first - Serializer.moshi.adapter(APIError::class.java).let { errorAdapter -> - errorAdapter.fromJson(text)?.let { parsedError -> - handleError( - ErrorResponse( - code = parsedError.code, - message = parsedError.message, - statusCode = parsedError.statusCode, - exceptionFields = parsedError.exceptionFields ?: emptyMap(), - moreInfo = parsedError.moreInfo, - ), - ) - } + Serializer.moshi.adapter(VideoEvent::class.java).let { eventAdapter -> + eventAdapter.fromJson(text)?.let { parsedEvent -> processEvent(parsedEvent) } } } catch (e: Throwable) { - // If parsing an APIError fails, try to parse a VideoEvent - // This will also catch unmapped events, that's why we parse APIError first - try { - Serializer.moshi.adapter(VideoEvent::class.java).let { eventAdapter -> - eventAdapter.fromJson(text)?.let { parsedEvent -> - if (parsedEvent is UnknownVideoEvent) { - logger.w { "[onMessage] Received unknown VideoEvent type: ${parsedEvent.expectedType}" } - } else { - processEvent(parsedEvent) - } - } - } - } catch (e: Throwable) { - logger.w { "[onMessage] VideoEvent parsing error ${e.message}" } + if (e.cause is UnsupportedVideoEventException) { + val ex = e.cause as UnsupportedVideoEventException + logger.w { "[onMessage] Received unsupported VideoEvent type: ${ex.type}. Ignoring." } + } else { + logger.w { "[onMessage] VideoEvent parsing error ${e.message}." } + handleError(e) } } } } private suspend fun processEvent(parsedEvent: VideoEvent) { + if (parsedEvent is ConnectionErrorEvent) { + handleError( + ErrorResponse( + code = parsedEvent.error?.code ?: -1, + message = parsedEvent.error?.message ?: "", + statusCode = parsedEvent.error?.statusCode ?: -1, + exceptionFields = parsedEvent.error?.exceptionFields ?: emptyMap(), + moreInfo = parsedEvent.error?.moreInfo ?: "", + ), + ) + return + } + if (parsedEvent is ConnectedEvent) { _connectionId.value = parsedEvent.connectionId setConnectedStateAndContinue(parsedEvent) diff --git a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt index 26ef962f78..15e66dd44f 100644 --- a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt +++ b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt @@ -168,8 +168,4 @@ class VideoEventAdapter : JsonAdapter() { } } -data class UnknownVideoEvent( - @Json(name = "type") val expectedType: String -) : VideoEvent() { - override fun getEventType() = "unknown" -} +class UnsupportedVideoEventException(val type: String) : Exception() From d44d256395a8b2f4f7b373b04703b00b016dde4d Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:15:16 +0300 Subject: [PATCH 4/4] Use exception in VideoEvent --- .../main/kotlin/org/openapitools/client/models/VideoEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt index 15e66dd44f..caaa237fda 100644 --- a/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt +++ b/stream-video-android-core/src/main/kotlin/org/openapitools/client/models/VideoEvent.kt @@ -163,7 +163,7 @@ class VideoEventAdapter : JsonAdapter() { "user.reactivated" -> UserReactivatedEvent::class.java "user.unbanned" -> UserUnbannedEvent::class.java "user.updated" -> UserUpdatedEvent::class.java - else -> UnknownVideoEvent(type)::class.java + else -> throw UnsupportedVideoEventException(type) } } }