diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessage.kt index 1208e8e6c39..6d05cb28729 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessage.kt @@ -15,9 +15,11 @@ import kotlinx.parcelize.Parcelize @Parcelize @JsonObject data class BaseWebSocketMessage( + @JsonField(name = ["id"]) + override var id: String? = null, @JsonField(name = ["type"]) - var type: String? = null -) : Parcelable { + override var type: String? = null +) : BaseWebSocketMessageInterface, Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null) + constructor() : this(null, null) } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessageInterface.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessageInterface.kt new file mode 100644 index 00000000000..bccb1a4862f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/BaseWebSocketMessageInterface.kt @@ -0,0 +1,15 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.models.json.websocket + +/** + * Interface with the properties common to all websocket signaling messages. + */ +interface BaseWebSocketMessageInterface { + var id: String? + var type: String? +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/CallOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/CallOverallWebSocketMessage.kt index 5e13a1a33e4..8855a61956a 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/websocket/CallOverallWebSocketMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/CallOverallWebSocketMessage.kt @@ -15,11 +15,13 @@ import kotlinx.parcelize.Parcelize @Parcelize @JsonObject data class CallOverallWebSocketMessage( + @JsonField(name = ["id"]) + override var id: String? = null, @JsonField(name = ["type"]) - var type: String? = null, + override var type: String? = null, @JsonField(name = ["message"]) var callWebSocketMessage: CallWebSocketMessage? = null -) : Parcelable { +) : BaseWebSocketMessageInterface, Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null, null) + constructor() : this(null, null, null) } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorOverallWebSocketMessage.kt index 90ceafdacd0..c56658af970 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorOverallWebSocketMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/ErrorOverallWebSocketMessage.kt @@ -15,11 +15,13 @@ import kotlinx.parcelize.Parcelize @Parcelize @JsonObject data class ErrorOverallWebSocketMessage( + @JsonField(name = ["id"]) + override var id: String? = null, @JsonField(name = ["type"]) - var type: String? = null, + override var type: String? = null, @JsonField(name = ["error"]) var errorWebSocketMessage: ErrorWebSocketMessage? = null -) : Parcelable { +) : BaseWebSocketMessageInterface, Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null, null) + constructor() : this(null, null, null) } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/EventOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/EventOverallWebSocketMessage.kt index 969cf2bb34f..ebfb3062e0e 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/websocket/EventOverallWebSocketMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/EventOverallWebSocketMessage.kt @@ -19,11 +19,13 @@ import java.util.HashMap @JsonObject @TypeParceler class EventOverallWebSocketMessage( + @JsonField(name = ["id"]) + override var id: String? = null, @JsonField(name = ["type"]) - var type: String? = null, + override var type: String? = null, @JsonField(name = ["event"]) var eventMap: HashMap? = null -) : Parcelable { +) : BaseWebSocketMessageInterface, Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null, null) + constructor() : this(null, null, null) } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloOverallWebSocketMessage.kt index 8d213df396c..ec7dd4b8c71 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloOverallWebSocketMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloOverallWebSocketMessage.kt @@ -15,11 +15,13 @@ import kotlinx.parcelize.Parcelize @Parcelize @JsonObject data class HelloOverallWebSocketMessage( + @JsonField(name = ["id"]) + override var id: String? = null, @JsonField(name = ["type"]) - var type: String? = null, + override var type: String? = null, @JsonField(name = ["hello"]) var helloWebSocketMessage: HelloWebSocketMessage? = null -) : Parcelable { +) : BaseWebSocketMessageInterface, Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null, null) + constructor() : this(null, null, null) } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseOverallWebSocketMessage.kt index 025bddf5135..41b1d7fa4bb 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseOverallWebSocketMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/HelloResponseOverallWebSocketMessage.kt @@ -15,11 +15,13 @@ import kotlinx.parcelize.Parcelize @Parcelize @JsonObject data class HelloResponseOverallWebSocketMessage( + @JsonField(name = ["id"]) + override var id: String? = null, @JsonField(name = ["type"]) - var type: String? = null, + override var type: String? = null, @JsonField(name = ["hello"]) var helloResponseWebSocketMessage: HelloResponseWebSocketMessage? = null -) : Parcelable { +) : BaseWebSocketMessageInterface, Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null, null) + constructor() : this(null, null, null) } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/JoinedRoomOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/JoinedRoomOverallWebSocketMessage.kt index 780367b236c..d22cb2db12f 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/websocket/JoinedRoomOverallWebSocketMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/JoinedRoomOverallWebSocketMessage.kt @@ -15,11 +15,13 @@ import kotlinx.parcelize.Parcelize @Parcelize @JsonObject data class JoinedRoomOverallWebSocketMessage( + @JsonField(name = ["id"]) + override var id: String? = null, @JsonField(name = ["type"]) - var type: String? = null, + override var type: String? = null, @JsonField(name = ["room"]) var roomWebSocketMessage: RoomWebSocketMessage? = null -) : Parcelable { +) : BaseWebSocketMessageInterface, Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null, null) + constructor() : this(null, null, null) } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomOverallWebSocketMessage.kt b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomOverallWebSocketMessage.kt index a347d18b83a..ce875aa76d6 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomOverallWebSocketMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomOverallWebSocketMessage.kt @@ -16,10 +16,12 @@ import kotlinx.parcelize.Parcelize @JsonObject class RoomOverallWebSocketMessage( @JsonField(name = ["type"]) - var type: String? = null, + override var id: String? = null, + @JsonField(name = ["type"]) + override var type: String? = null, @JsonField(name = ["room"]) var roomWebSocketMessage: RoomWebSocketMessage? = null -) : Parcelable { +) : BaseWebSocketMessageInterface, Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' - constructor() : this(null, null) + constructor() : this(null, null, null) } diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt index c6d1a9df254..86e6b1c1390 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt +++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt @@ -21,6 +21,7 @@ import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.Participant.ActorType import com.nextcloud.talk.models.json.signaling.NCSignalingMessage import com.nextcloud.talk.models.json.signaling.settings.FederationSettings +import com.nextcloud.talk.models.json.websocket.BaseWebSocketMessageInterface import com.nextcloud.talk.models.json.websocket.BaseWebSocketMessage import com.nextcloud.talk.models.json.websocket.ByeWebSocketMessage import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage @@ -79,7 +80,9 @@ class WebSocketInstance internal constructor( private var currentFederation: FederationSettings? = null private var reconnecting = false private val usersHashMap: HashMap - private var messagesQueue: MutableList = ArrayList() + private var messagesQueue: MutableList Unit)?>> = ArrayList() + private var callbacks: MutableMap Unit> = HashMap() + private var lastCallbackId: Int = 1 private val signalingMessageReceiver = ExternalSignalingMessageReceiver() val signalingMessageSender = ExternalSignalingMessageSender() @@ -140,6 +143,7 @@ class WebSocketInstance internal constructor( fun restartWebSocket() { reconnecting = true + callbacks.clear() Log.d(TAG, "restartWebSocket: $connectionUrl") val request = Request.Builder().url(connectionUrl).build() okHttpClient!!.newWebSocket(request, this) @@ -149,12 +153,18 @@ class WebSocketInstance internal constructor( if (webSocket === internalWebSocket) { Log.d(TAG, "Receiving : $webSocket $text") try { - val (messageType) = LoganSquare.parse(text, BaseWebSocketMessage::class.java) + val (id, messageType) = LoganSquare.parse(text, BaseWebSocketMessage::class.java) + + if (callbacks.contains(id)) { + val callback = callbacks[id]!! + callbacks.remove(id) + callback(text) + } + if (messageType != null) { when (messageType) { "hello" -> processHelloMessage(webSocket, text) "error" -> processErrorMessage(webSocket, text) - "room" -> processJoinedRoomMessage(text) "event" -> processEventMessage(text) "message" -> processMessage(text) "bye" -> { @@ -175,7 +185,7 @@ class WebSocketInstance internal constructor( @Throws(IOException::class) private fun processMessage(text: String) { - val (_, callWebSocketMessage) = LoganSquare.parse(text, CallOverallWebSocketMessage::class.java) + val (_, _, callWebSocketMessage) = LoganSquare.parse(text, CallOverallWebSocketMessage::class.java) if (callWebSocketMessage != null) { val ncSignalingMessage = callWebSocketMessage.ncSignalingMessage @@ -284,7 +294,7 @@ class WebSocketInstance internal constructor( @Throws(IOException::class) private fun processJoinedRoomMessage(text: String) { - val (_, roomWebSocketMessage) = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage::class.java) + val (_, _, roomWebSocketMessage) = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage::class.java) if (roomWebSocketMessage != null) { currentRoomToken = roomWebSocketMessage.roomId if (roomWebSocketMessage.roomPropertiesWebSocketMessage != null && !TextUtils.isEmpty(currentRoomToken)) { @@ -296,7 +306,7 @@ class WebSocketInstance internal constructor( @Throws(IOException::class) private fun processErrorMessage(webSocket: WebSocket, text: String) { Log.e(TAG, "Received error: $text") - val (_, message) = LoganSquare.parse(text, ErrorOverallWebSocketMessage::class.java) + val (_, _, message) = LoganSquare.parse(text, ErrorOverallWebSocketMessage::class.java) if (message != null) { if ("no_such_session" == message.code) { Log.d(TAG, "WebSocket " + webSocket.hashCode() + " resumeID " + resumeId + " expired") @@ -315,7 +325,7 @@ class WebSocketInstance internal constructor( isConnected = true reconnecting = false val oldResumeId = resumeId - val (_, helloResponseWebSocketMessage1) = LoganSquare.parse( + val (_, _, helloResponseWebSocketMessage1) = LoganSquare.parse( text, HelloResponseOverallWebSocketMessage::class.java ) @@ -325,7 +335,12 @@ class WebSocketInstance internal constructor( hasMCU = helloResponseWebSocketMessage1.serverHasMCUSupport() } for (i in messagesQueue.indices) { - webSocket.send(messagesQueue[i]) + // Safety check to ensure that it will not end in an endless loop + // trying to send the messages, queueing them, and then trying to + // send them again and again. + if (isConnected && !reconnecting) { + sendMessage(messagesQueue[i].first, messagesQueue[i].second) + } } messagesQueue = ArrayList() val helloHashMap = HashMap() @@ -379,14 +394,15 @@ class WebSocketInstance internal constructor( Log.d(TAG, " roomToken: $roomToken") Log.d(TAG, " session: $normalBackendSession") try { - val message = LoganSquare.serialize( - webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken, normalBackendSession, federation) - ) + val message = webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken, normalBackendSession, federation) + val processJoinedRoomMessageCallback = { text: String -> + processJoinedRoomMessage(text) + } if (roomToken == "") { Log.d(TAG, "sending 'leave room' via websocket") currentNormalBackendSession = "" currentFederation = null - sendMessage(message) + sendMessage(message, processJoinedRoomMessageCallback) } else if ( roomToken == currentRoomToken && normalBackendSession == currentNormalBackendSession && @@ -399,7 +415,7 @@ class WebSocketInstance internal constructor( Log.d(TAG, "Sending join room message via websocket") currentNormalBackendSession = normalBackendSession currentFederation = federation - sendMessage(message) + sendMessage(message, processJoinedRoomMessageCallback) } } catch (e: IOException) { Log.e(TAG, "Failed to serialize signaling message", e) @@ -408,27 +424,33 @@ class WebSocketInstance internal constructor( private fun sendCallMessage(ncSignalingMessage: NCSignalingMessage) { try { - val message = LoganSquare.serialize( - webSocketConnectionHelper.getAssembledCallMessageModel(ncSignalingMessage) - ) + val message = webSocketConnectionHelper.getAssembledCallMessageModel(ncSignalingMessage) sendMessage(message) } catch (e: IOException) { Log.e(TAG, "Failed to serialize signaling message", e) } } - private fun sendMessage(message: String) { + private fun sendMessage(message: BaseWebSocketMessageInterface, callback: ((String) -> Unit)? = null) { if (!isConnected || reconnecting) { - messagesQueue.add(message) + messagesQueue.add(Pair(message, callback)) if (!reconnecting) { restartWebSocket() } - } else { - if (!internalWebSocket!!.send(message)) { - messagesQueue.add(message) - restartWebSocket() - } + + return + } + + if (callback != null) { + val callbackId = lastCallbackId++ + callbacks[callbackId.toString()] = callback + message.id = callbackId.toString() + } + + if (!internalWebSocket!!.send(LoganSquare.serialize(message))) { + messagesQueue.add(Pair(message, callback)) + restartWebSocket() } }