mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-20 20:19:42 +01:00
530 lines
18 KiB
Kotlin
530 lines
18 KiB
Kotlin
/*
|
|
* Nextcloud Talk application
|
|
*
|
|
* @author Mario Danic
|
|
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package com.nextcloud.talk.webrtc
|
|
|
|
import android.content.Context
|
|
import android.text.TextUtils
|
|
import android.util.Log
|
|
import autodagger.AutoInjector
|
|
import com.bluelinelabs.logansquare.LoganSquare
|
|
import com.nextcloud.talk.R.string
|
|
import com.nextcloud.talk.application.NextcloudTalkApplication
|
|
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
|
import com.nextcloud.talk.events.NetworkEvent
|
|
import com.nextcloud.talk.events.NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED
|
|
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
|
import com.nextcloud.talk.models.json.participants.Participant
|
|
import com.nextcloud.talk.models.json.signaling.NCMessageWrapper
|
|
import com.nextcloud.talk.models.json.websocket.BaseWebSocketMessage
|
|
import com.nextcloud.talk.models.json.websocket.ByeWebSocketMessage
|
|
import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage
|
|
import com.nextcloud.talk.models.json.websocket.ErrorOverallWebSocketMessage
|
|
import com.nextcloud.talk.models.json.websocket.EventOverallWebSocketMessage
|
|
import com.nextcloud.talk.models.json.websocket.HelloResponseOverallWebSocketMessage
|
|
import com.nextcloud.talk.models.json.websocket.JoinedRoomOverallWebSocketMessage
|
|
import com.nextcloud.talk.newarch.local.models.UserNgEntity
|
|
import com.nextcloud.talk.utils.LoggingUtils.writeLogEntryToFile
|
|
import com.nextcloud.talk.utils.MagicMap
|
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
|
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.Request.Builder
|
|
import okhttp3.Response
|
|
import okhttp3.WebSocket
|
|
import okhttp3.WebSocketListener
|
|
import okio.ByteString
|
|
import org.greenrobot.eventbus.EventBus
|
|
import org.greenrobot.eventbus.Subscribe
|
|
import org.greenrobot.eventbus.ThreadMode.BACKGROUND
|
|
import org.koin.core.KoinComponent
|
|
import org.koin.core.inject
|
|
import java.io.IOException
|
|
import java.util.ArrayList
|
|
import java.util.HashMap
|
|
import javax.inject.Inject
|
|
|
|
@AutoInjector(NextcloudTalkApplication::class)
|
|
class MagicWebSocketInstance internal constructor(
|
|
conversationUser: UserNgEntity,
|
|
connectionUrl: String,
|
|
webSocketTicket: String
|
|
) : WebSocketListener(), KoinComponent {
|
|
val okHttpClient: OkHttpClient by inject()
|
|
val eventBus: EventBus by inject()
|
|
val context: Context by inject()
|
|
|
|
private val conversationUser: UserNgEntity
|
|
private val webSocketTicket: String
|
|
private var resumeId: String? = null
|
|
var sessionId: String? = null
|
|
private set
|
|
private var hasMCU = false
|
|
var isConnected: Boolean
|
|
private set
|
|
private val webSocketConnectionHelper: WebSocketConnectionHelper
|
|
private var internalWebSocket: WebSocket? = null
|
|
private val magicMap: MagicMap
|
|
private val connectionUrl: String
|
|
private var currentRoomToken: String? = null
|
|
private var restartCount = 0
|
|
private var reconnecting = false
|
|
private val usersHashMap: HashMap<String?, Participant>
|
|
private var messagesQueue: MutableList<String> =
|
|
ArrayList()
|
|
|
|
private fun sendHello() {
|
|
try {
|
|
if (TextUtils.isEmpty(resumeId)) {
|
|
internalWebSocket!!.send(
|
|
LoganSquare.serialize(
|
|
webSocketConnectionHelper.getAssembledHelloModel(conversationUser, webSocketTicket)
|
|
)
|
|
)
|
|
} else {
|
|
internalWebSocket!!.send(
|
|
LoganSquare.serialize(
|
|
webSocketConnectionHelper.getAssembledHelloModelForResume(resumeId)
|
|
)
|
|
)
|
|
}
|
|
} catch (e: IOException) {
|
|
Log.e(TAG, "Failed to serialize hello model")
|
|
}
|
|
}
|
|
|
|
override fun onOpen(
|
|
webSocket: WebSocket,
|
|
response: Response
|
|
) {
|
|
internalWebSocket = webSocket
|
|
sendHello()
|
|
}
|
|
|
|
private fun closeWebSocket(webSocket: WebSocket) {
|
|
webSocket.close(1000, null)
|
|
webSocket.cancel()
|
|
if (webSocket === internalWebSocket) {
|
|
isConnected = false
|
|
messagesQueue = ArrayList()
|
|
}
|
|
restartWebSocket()
|
|
}
|
|
|
|
fun clearResumeId() {
|
|
resumeId = ""
|
|
}
|
|
|
|
fun restartWebSocket() {
|
|
reconnecting = true
|
|
val request = Builder()
|
|
.url(connectionUrl)
|
|
.build()
|
|
okHttpClient.newWebSocket(request, this)
|
|
restartCount++
|
|
}
|
|
|
|
override fun onMessage(
|
|
webSocket: WebSocket,
|
|
text: String
|
|
) {
|
|
if (webSocket === internalWebSocket) {
|
|
Log.d(
|
|
TAG, "Receiving : $webSocket $text"
|
|
)
|
|
writeLogEntryToFile(
|
|
context,
|
|
"WebSocket " + webSocket.hashCode() + " receiving: " + text
|
|
)
|
|
try {
|
|
val baseWebSocketMessage =
|
|
LoganSquare.parse(text, BaseWebSocketMessage::class.java)
|
|
val messageType = baseWebSocketMessage.type
|
|
when (messageType) {
|
|
"hello" -> {
|
|
isConnected = true
|
|
reconnecting = false
|
|
restartCount = 0
|
|
val oldResumeId = resumeId
|
|
val helloResponseWebSocketMessage =
|
|
LoganSquare.parse(
|
|
text, HelloResponseOverallWebSocketMessage::class.java
|
|
)
|
|
resumeId = helloResponseWebSocketMessage.helloResponseWebSocketMessage
|
|
.resumeId
|
|
sessionId = helloResponseWebSocketMessage.helloResponseWebSocketMessage
|
|
.sessionId
|
|
hasMCU = helloResponseWebSocketMessage.helloResponseWebSocketMessage
|
|
.serverHasMCUSupport()
|
|
var i = 0
|
|
while (i < messagesQueue.size) {
|
|
webSocket.send(messagesQueue[i])
|
|
i++
|
|
}
|
|
messagesQueue = ArrayList()
|
|
val helloHasHap =
|
|
HashMap<String, String?>()
|
|
if (!TextUtils.isEmpty(oldResumeId)) {
|
|
helloHasHap["oldResumeId"] = oldResumeId
|
|
} else {
|
|
currentRoomToken = ""
|
|
}
|
|
if (!TextUtils.isEmpty(currentRoomToken)) {
|
|
helloHasHap["roomToken"] = currentRoomToken
|
|
}
|
|
eventBus.post(WebSocketCommunicationEvent("hello", helloHasHap))
|
|
}
|
|
"error" -> {
|
|
val errorOverallWebSocketMessage =
|
|
LoganSquare.parse(
|
|
text, ErrorOverallWebSocketMessage::class.java
|
|
)
|
|
if ("no_such_session" ==
|
|
errorOverallWebSocketMessage.errorWebSocketMessage.code
|
|
) {
|
|
writeLogEntryToFile(
|
|
context,
|
|
"WebSocket " + webSocket.hashCode() + " resumeID " + resumeId + " expired"
|
|
)
|
|
resumeId = ""
|
|
currentRoomToken = ""
|
|
restartWebSocket()
|
|
} else if ("hello_expected" ==
|
|
errorOverallWebSocketMessage.errorWebSocketMessage.code
|
|
) {
|
|
restartWebSocket()
|
|
}
|
|
}
|
|
"room" -> {
|
|
val joinedRoomOverallWebSocketMessage =
|
|
LoganSquare.parse(
|
|
text, JoinedRoomOverallWebSocketMessage::class.java
|
|
)
|
|
currentRoomToken = joinedRoomOverallWebSocketMessage.roomWebSocketMessage
|
|
.roomId
|
|
if (joinedRoomOverallWebSocketMessage.roomWebSocketMessage
|
|
.roomPropertiesWebSocketMessage != null && !TextUtils.isEmpty(
|
|
currentRoomToken
|
|
)
|
|
) {
|
|
sendRoomJoinedEvent()
|
|
}
|
|
}
|
|
"event" -> {
|
|
val eventOverallWebSocketMessage =
|
|
LoganSquare.parse(
|
|
text, EventOverallWebSocketMessage::class.java
|
|
)
|
|
if (eventOverallWebSocketMessage.eventMap != null) {
|
|
val target =
|
|
eventOverallWebSocketMessage.eventMap["target"] as String?
|
|
when (target) {
|
|
"room" -> if (eventOverallWebSocketMessage.eventMap["type"] == "message"
|
|
) {
|
|
val messageHashMap =
|
|
eventOverallWebSocketMessage.eventMap["message"] as Map<String, Any>?
|
|
if (messageHashMap!!.containsKey("data")) {
|
|
val dataHashMap =
|
|
messageHashMap["data"] as Map<String, Any>?
|
|
if (dataHashMap!!.containsKey("chat")) {
|
|
val shouldRefreshChat: Boolean
|
|
val chatMap =
|
|
dataHashMap["chat"] as Map<String, Any>?
|
|
if (chatMap!!.containsKey("refresh")) {
|
|
shouldRefreshChat = chatMap["refresh"] as Boolean
|
|
if (shouldRefreshChat) {
|
|
val refreshChatHashMap =
|
|
HashMap<String, String?>()
|
|
refreshChatHashMap[KEY_ROOM_TOKEN] = messageHashMap["roomid"] as String?
|
|
refreshChatHashMap[KEY_INTERNAL_USER_ID] =
|
|
java.lang.Long.toString(conversationUser.id!!)
|
|
eventBus.post(
|
|
WebSocketCommunicationEvent("refreshChat", refreshChatHashMap)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (eventOverallWebSocketMessage.eventMap["type"]
|
|
== "join"
|
|
) {
|
|
val joinEventMap =
|
|
eventOverallWebSocketMessage.eventMap["join"] as List<HashMap<String, Any>>?
|
|
var internalHashMap: HashMap<String, Any>
|
|
var participant: Participant
|
|
var i = 0
|
|
while (i < joinEventMap!!.size) {
|
|
internalHashMap = joinEventMap[i]
|
|
val userMap =
|
|
internalHashMap["user"] as HashMap<String, Any>?
|
|
participant = Participant()
|
|
participant.userId = internalHashMap["userid"] as String
|
|
participant.displayName = userMap!!["displayname"] as String
|
|
usersHashMap[internalHashMap["sessionid"] as String?] = participant
|
|
i++
|
|
}
|
|
}
|
|
"participants" -> if (eventOverallWebSocketMessage.eventMap["type"] == "update"
|
|
) {
|
|
val refreshChatHashMap =
|
|
HashMap<String, String?>()
|
|
val updateEventMap =
|
|
eventOverallWebSocketMessage.eventMap["update"] as HashMap<String, Any>?
|
|
refreshChatHashMap["roomToken"] = updateEventMap!!["roomid"] as String?
|
|
refreshChatHashMap["jobId"] = Integer.toString(
|
|
magicMap.add(
|
|
updateEventMap["users"]!!
|
|
)
|
|
)
|
|
eventBus.post(
|
|
WebSocketCommunicationEvent("participantsUpdate", refreshChatHashMap)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"message" -> {
|
|
val callOverallWebSocketMessage =
|
|
LoganSquare.parse(
|
|
text, CallOverallWebSocketMessage::class.java
|
|
)
|
|
val ncSignalingMessage =
|
|
callOverallWebSocketMessage.callWebSocketMessage
|
|
.ncSignalingMessage
|
|
if (TextUtils.isEmpty(ncSignalingMessage.from)
|
|
&& callOverallWebSocketMessage.callWebSocketMessage.senderWebSocketMessage
|
|
!= null
|
|
) {
|
|
ncSignalingMessage.from =
|
|
callOverallWebSocketMessage.callWebSocketMessage
|
|
.senderWebSocketMessage
|
|
.sessionId
|
|
|
|
}
|
|
if (!TextUtils.isEmpty(ncSignalingMessage.from)) {
|
|
val messageHashMap =
|
|
HashMap<String, String>()
|
|
messageHashMap["jobId"] = Integer.toString(magicMap.add(ncSignalingMessage))
|
|
eventBus.post(WebSocketCommunicationEvent("signalingMessage", messageHashMap))
|
|
}
|
|
}
|
|
"bye" -> {
|
|
isConnected = false
|
|
resumeId = ""
|
|
}
|
|
else -> {
|
|
}
|
|
}
|
|
} catch (e: IOException) {
|
|
writeLogEntryToFile(
|
|
context,
|
|
"WebSocket " + webSocket.hashCode() + " IOException: " + e.message
|
|
)
|
|
Log.e(
|
|
TAG, "Failed to recognize WebSocket message"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun sendRoomJoinedEvent() {
|
|
val joinRoomHashMap =
|
|
HashMap<String, String?>()
|
|
joinRoomHashMap["roomToken"] = currentRoomToken
|
|
eventBus.post(WebSocketCommunicationEvent("roomJoined", joinRoomHashMap))
|
|
}
|
|
|
|
override fun onMessage(
|
|
webSocket: WebSocket,
|
|
bytes: ByteString
|
|
) {
|
|
Log.d(TAG, "Receiving bytes : " + bytes.hex())
|
|
}
|
|
|
|
override fun onClosing(
|
|
webSocket: WebSocket,
|
|
code: Int,
|
|
reason: String
|
|
) {
|
|
Log.d(TAG, "Closing : $code / $reason")
|
|
writeLogEntryToFile(
|
|
context,
|
|
"WebSocket " + webSocket.hashCode() + " Closing: " + reason
|
|
)
|
|
}
|
|
|
|
override fun onFailure(
|
|
webSocket: WebSocket,
|
|
t: Throwable,
|
|
response: Response?
|
|
) {
|
|
Log.d(TAG, "Error : " + t.message)
|
|
writeLogEntryToFile(
|
|
context,
|
|
"WebSocket " + webSocket.hashCode() + " onFailure: " + t.message
|
|
)
|
|
closeWebSocket(webSocket)
|
|
}
|
|
|
|
fun hasMCU(): Boolean {
|
|
return hasMCU
|
|
}
|
|
|
|
fun joinRoomWithRoomTokenAndSession(
|
|
roomToken: String,
|
|
normalBackendSession: String?
|
|
) {
|
|
try {
|
|
val message = LoganSquare.serialize(
|
|
webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(
|
|
roomToken,
|
|
normalBackendSession
|
|
)
|
|
)
|
|
if (!isConnected || reconnecting) {
|
|
messagesQueue.add(message)
|
|
} else {
|
|
if (roomToken == currentRoomToken) {
|
|
sendRoomJoinedEvent()
|
|
} else {
|
|
internalWebSocket!!.send(message)
|
|
}
|
|
}
|
|
} catch (e: IOException) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
fun sendCallMessage(ncMessageWrapper: NCMessageWrapper) {
|
|
try {
|
|
val message = LoganSquare.serialize(
|
|
webSocketConnectionHelper.getAssembledCallMessageModel(ncMessageWrapper)
|
|
)
|
|
if (!isConnected || reconnecting) {
|
|
messagesQueue.add(message)
|
|
} else {
|
|
internalWebSocket!!.send(message)
|
|
}
|
|
} catch (e: IOException) {
|
|
writeLogEntryToFile(
|
|
context,
|
|
"WebSocket sendCalLMessage: " + e.message + "\n" + ncMessageWrapper.toString()
|
|
)
|
|
Log.e(
|
|
TAG, "Failed to serialize signaling message"
|
|
)
|
|
}
|
|
}
|
|
|
|
fun getJobWithId(id: Int): Any? {
|
|
val copyJob = magicMap[id]
|
|
magicMap.remove(id)
|
|
return copyJob
|
|
}
|
|
|
|
fun requestOfferForSessionIdWithType(
|
|
sessionIdParam: String,
|
|
roomType: String
|
|
) {
|
|
try {
|
|
val message = LoganSquare.serialize(
|
|
webSocketConnectionHelper.getAssembledRequestOfferModel(sessionIdParam, roomType)
|
|
)
|
|
if (!isConnected || reconnecting) {
|
|
messagesQueue.add(message)
|
|
} else {
|
|
internalWebSocket!!.send(message)
|
|
}
|
|
} catch (e: IOException) {
|
|
writeLogEntryToFile(
|
|
context,
|
|
"WebSocket requestOfferForSessionIdWithType: "
|
|
+ e.message
|
|
+ "\n"
|
|
+ sessionIdParam
|
|
+ " "
|
|
+ roomType
|
|
)
|
|
Log.e(TAG, "Failed to offer request")
|
|
}
|
|
}
|
|
|
|
fun sendBye() {
|
|
if (isConnected) {
|
|
try {
|
|
val byeWebSocketMessage = ByeWebSocketMessage()
|
|
byeWebSocketMessage.type = "bye"
|
|
byeWebSocketMessage.bye = HashMap()
|
|
internalWebSocket!!.send(LoganSquare.serialize(byeWebSocketMessage))
|
|
} catch (e: IOException) {
|
|
Log.e(TAG, "Failed to serialize bye message")
|
|
}
|
|
}
|
|
}
|
|
|
|
fun getDisplayNameForSession(session: String?): String {
|
|
return if (usersHashMap.containsKey(session)) {
|
|
usersHashMap[session]!!.displayName
|
|
} else sharedApplication!!.getString(string.nc_nick_guest)
|
|
}
|
|
|
|
fun getSessionForUserId(userId: String): String? {
|
|
for (session in usersHashMap.keys) {
|
|
if (userId == usersHashMap[session]!!.userId) {
|
|
return session
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
fun getUserIdForSession(session: String?): String {
|
|
return if (usersHashMap.containsKey(session)) {
|
|
usersHashMap[session]!!.userId
|
|
} else ""
|
|
}
|
|
|
|
@Subscribe(threadMode = BACKGROUND)
|
|
fun onMessageEvent(networkEvent: NetworkEvent) {
|
|
if ((networkEvent.networkConnectionEvent
|
|
== NETWORK_CONNECTED) && !isConnected
|
|
) {
|
|
restartWebSocket()
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
private const val TAG = "MagicWebSocketInstance"
|
|
}
|
|
|
|
init {
|
|
sharedApplication
|
|
?.componentApplication
|
|
?.inject(this)
|
|
this.connectionUrl = connectionUrl
|
|
this.conversationUser = conversationUser
|
|
this.webSocketTicket = webSocketTicket
|
|
webSocketConnectionHelper = WebSocketConnectionHelper()
|
|
usersHashMap =
|
|
HashMap()
|
|
magicMap = MagicMap()
|
|
isConnected = false
|
|
eventBus.register(this)
|
|
restartWebSocket()
|
|
}
|
|
} |