MagicWebSocketInstance.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. package com.nextcloud.talk.webrtc;
  21. import android.content.Context;
  22. import android.text.TextUtils;
  23. import android.util.Log;
  24. import autodagger.AutoInjector;
  25. import com.bluelinelabs.logansquare.LoganSquare;
  26. import com.nextcloud.talk.R;
  27. import com.nextcloud.talk.application.NextcloudTalkApplication;
  28. import com.nextcloud.talk.events.NetworkEvent;
  29. import com.nextcloud.talk.events.WebSocketCommunicationEvent;
  30. import com.nextcloud.talk.models.database.UserEntity;
  31. import com.nextcloud.talk.models.json.participants.Participant;
  32. import com.nextcloud.talk.models.json.signaling.NCMessageWrapper;
  33. import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
  34. import com.nextcloud.talk.models.json.websocket.*;
  35. import com.nextcloud.talk.utils.LoggingUtils;
  36. import com.nextcloud.talk.utils.MagicMap;
  37. import com.nextcloud.talk.utils.singletons.MerlinTheWizard;
  38. import okhttp3.*;
  39. import okio.ByteString;
  40. import org.greenrobot.eventbus.EventBus;
  41. import org.greenrobot.eventbus.Subscribe;
  42. import org.greenrobot.eventbus.ThreadMode;
  43. import javax.inject.Inject;
  44. import java.io.IOException;
  45. import java.util.ArrayList;
  46. import java.util.HashMap;
  47. import java.util.List;
  48. import java.util.Map;
  49. @AutoInjector(NextcloudTalkApplication.class)
  50. public class MagicWebSocketInstance extends WebSocketListener {
  51. private static final String TAG = "MagicWebSocketInstance";
  52. @Inject
  53. OkHttpClient okHttpClient;
  54. @Inject
  55. EventBus eventBus;
  56. @Inject
  57. Context context;
  58. private UserEntity conversationUser;
  59. private String webSocketTicket;
  60. private String resumeId;
  61. private String sessionId;
  62. private boolean hasMCU;
  63. private boolean connected;
  64. private WebSocketConnectionHelper webSocketConnectionHelper;
  65. private WebSocket internalWebSocket;
  66. private MagicMap magicMap;
  67. private String connectionUrl;
  68. private String currentRoomToken;
  69. private int restartCount = 0;
  70. private boolean reconnecting = false;
  71. private HashMap<String, Participant> usersHashMap;
  72. private List<String> messagesQueue = new ArrayList<>();
  73. MagicWebSocketInstance(UserEntity conversationUser, String connectionUrl, String webSocketTicket) {
  74. NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this);
  75. this.connectionUrl = connectionUrl;
  76. this.conversationUser = conversationUser;
  77. this.webSocketTicket = webSocketTicket;
  78. this.webSocketConnectionHelper = new WebSocketConnectionHelper();
  79. this.usersHashMap = new HashMap<>();
  80. magicMap = new MagicMap();
  81. connected = false;
  82. eventBus.register(this);
  83. restartWebSocket();
  84. }
  85. private void sendHello() {
  86. try {
  87. if (TextUtils.isEmpty(resumeId)) {
  88. internalWebSocket.send(LoganSquare.serialize(webSocketConnectionHelper.getAssembledHelloModel(conversationUser, webSocketTicket)));
  89. } else {
  90. internalWebSocket.send(LoganSquare.serialize(webSocketConnectionHelper.getAssembledHelloModelForResume(resumeId)));
  91. }
  92. } catch (IOException e) {
  93. Log.e(TAG, "Failed to serialize hello model");
  94. }
  95. }
  96. @Override
  97. public void onOpen(WebSocket webSocket, Response response) {
  98. internalWebSocket = webSocket;
  99. sendHello();
  100. }
  101. private void closeWebSocket(WebSocket webSocket) {
  102. webSocket.close(1000, null);
  103. webSocket.cancel();
  104. if (webSocket == internalWebSocket) {
  105. connected = false;
  106. messagesQueue = new ArrayList<>();
  107. }
  108. restartWebSocket();
  109. }
  110. public void clearResumeId() {
  111. resumeId = "";
  112. }
  113. public void restartWebSocket() {
  114. reconnecting = true;
  115. if (MerlinTheWizard.isConnectedToInternet()) {
  116. Request request = new Request.Builder().url(connectionUrl).build();
  117. okHttpClient.newWebSocket(request, this);
  118. restartCount++;
  119. }
  120. }
  121. @Override
  122. public void onMessage(WebSocket webSocket, String text) {
  123. if (webSocket == internalWebSocket) {
  124. Log.d(TAG, "Receiving : " + webSocket.toString() + " " + text);
  125. LoggingUtils.writeLogEntryToFile(context,
  126. "WebSocket " + webSocket.hashCode() + " receiving: " + text);
  127. try {
  128. BaseWebSocketMessage baseWebSocketMessage = LoganSquare.parse(text, BaseWebSocketMessage.class);
  129. String messageType = baseWebSocketMessage.getType();
  130. switch (messageType) {
  131. case "hello":
  132. connected = true;
  133. reconnecting = false;
  134. restartCount = 0;
  135. String oldResumeId = resumeId;
  136. HelloResponseOverallWebSocketMessage helloResponseWebSocketMessage = LoganSquare.parse(text, HelloResponseOverallWebSocketMessage.class);
  137. resumeId = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getResumeId();
  138. sessionId = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getSessionId();
  139. hasMCU = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().serverHasMCUSupport();
  140. for (int i = 0; i < messagesQueue.size(); i++) {
  141. webSocket.send(messagesQueue.get(i));
  142. }
  143. messagesQueue = new ArrayList<>();
  144. HashMap<String, String> helloHasHap = new HashMap<>();
  145. if (!TextUtils.isEmpty(oldResumeId)) {
  146. helloHasHap.put("oldResumeId", oldResumeId);
  147. } else {
  148. currentRoomToken = "";
  149. }
  150. if (!TextUtils.isEmpty(currentRoomToken)) {
  151. helloHasHap.put("roomToken", currentRoomToken);
  152. }
  153. eventBus.post(new WebSocketCommunicationEvent("hello", helloHasHap));
  154. break;
  155. case "error":
  156. ErrorOverallWebSocketMessage errorOverallWebSocketMessage = LoganSquare.parse(text, ErrorOverallWebSocketMessage.class);
  157. if (("no_such_session").equals(errorOverallWebSocketMessage.getErrorWebSocketMessage().getCode())) {
  158. LoggingUtils.writeLogEntryToFile(context,
  159. "WebSocket " + webSocket.hashCode() + " resumeID " + resumeId + " expired");
  160. resumeId = "";
  161. currentRoomToken = "";
  162. restartWebSocket();
  163. } else if (("hello_expected").equals(errorOverallWebSocketMessage.getErrorWebSocketMessage().getCode())) {
  164. restartWebSocket();
  165. }
  166. break;
  167. case "room":
  168. JoinedRoomOverallWebSocketMessage joinedRoomOverallWebSocketMessage = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage.class);
  169. currentRoomToken = joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage().getRoomId();
  170. if (joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage().getRoomPropertiesWebSocketMessage() != null && !TextUtils.isEmpty(currentRoomToken)) {
  171. sendRoomJoinedEvent();
  172. }
  173. break;
  174. case "event":
  175. EventOverallWebSocketMessage eventOverallWebSocketMessage = LoganSquare.parse(text, EventOverallWebSocketMessage.class);
  176. if (eventOverallWebSocketMessage.getEventMap() != null) {
  177. String target = (String) eventOverallWebSocketMessage.getEventMap().get("target");
  178. switch (target) {
  179. case "room":
  180. if (eventOverallWebSocketMessage.getEventMap().get("type").equals("message")) {
  181. if (eventOverallWebSocketMessage.getEventMap().containsKey("data")) {
  182. Map<String, Object> dataHashMap = (Map<String, Object>) eventOverallWebSocketMessage.getEventMap().get("data");
  183. if (dataHashMap.containsKey("chat")) {
  184. boolean shouldRefreshChat;
  185. Map<String, Object> chatMap = (Map<String, Object>) dataHashMap.get("chat");
  186. if (chatMap.containsKey("refresh")) {
  187. shouldRefreshChat = (boolean) chatMap.get("refresh");
  188. if (shouldRefreshChat) {
  189. HashMap<String, String> refreshChatHashMap = new HashMap<>();
  190. refreshChatHashMap.put("roomToken", (String) eventOverallWebSocketMessage.getEventMap().get("roomid"));
  191. eventBus.post(new WebSocketCommunicationEvent("refreshChat", refreshChatHashMap));
  192. }
  193. }
  194. }
  195. }
  196. } else if (eventOverallWebSocketMessage.getEventMap().get("type").equals("join")) {
  197. List<HashMap<String, Object>> joinEventMap = (List<HashMap<String, Object>>) eventOverallWebSocketMessage.getEventMap().get("join");
  198. HashMap<String, Object> internalHashMap;
  199. Participant participant;
  200. for (int i = 0; i < joinEventMap.size(); i++) {
  201. internalHashMap = joinEventMap.get(i);
  202. HashMap<String, Object> userMap = (HashMap<String, Object>) internalHashMap.get("user");
  203. participant = new Participant();
  204. participant.setUserId((String) internalHashMap.get("userid"));
  205. participant.setDisplayName((String) userMap.get("displayname"));
  206. usersHashMap.put((String) internalHashMap.get("sessionid"), participant);
  207. }
  208. }
  209. break;
  210. case "participants":
  211. if (eventOverallWebSocketMessage.getEventMap().get("type").equals("update")) {
  212. HashMap<String, String> refreshChatHashMap = new HashMap<>();
  213. HashMap<String, Object> updateEventMap = (HashMap<String, Object>) eventOverallWebSocketMessage.getEventMap().get("update");
  214. refreshChatHashMap.put("roomToken", (String) updateEventMap.get("roomid"));
  215. refreshChatHashMap.put("jobId", Integer.toString(magicMap.add(updateEventMap.get("users"))));
  216. eventBus.post(new WebSocketCommunicationEvent("participantsUpdate", refreshChatHashMap));
  217. }
  218. break;
  219. }
  220. }
  221. break;
  222. case "message":
  223. CallOverallWebSocketMessage callOverallWebSocketMessage = LoganSquare.parse(text, CallOverallWebSocketMessage.class);
  224. NCSignalingMessage ncSignalingMessage = callOverallWebSocketMessage.getCallWebSocketMessage().getNcSignalingMessage();
  225. if (TextUtils.isEmpty(ncSignalingMessage.getFrom()) && callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage() != null) {
  226. ncSignalingMessage.setFrom(callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage().getSessionId());
  227. }
  228. if (!TextUtils.isEmpty(ncSignalingMessage.getFrom())) {
  229. HashMap<String, String> messageHashMap = new HashMap<>();
  230. messageHashMap.put("jobId", Integer.toString(magicMap.add(ncSignalingMessage)));
  231. eventBus.post(new WebSocketCommunicationEvent("signalingMessage", messageHashMap));
  232. }
  233. break;
  234. case "bye":
  235. connected = false;
  236. resumeId = "";
  237. default:
  238. break;
  239. }
  240. } catch (IOException e) {
  241. LoggingUtils.writeLogEntryToFile(context,
  242. "WebSocket " + webSocket.hashCode() + " IOException: " + e.getMessage());
  243. Log.e(TAG, "Failed to recognize WebSocket message");
  244. }
  245. }
  246. }
  247. private void sendRoomJoinedEvent() {
  248. HashMap<String, String> joinRoomHashMap = new HashMap<>();
  249. joinRoomHashMap.put("roomToken", currentRoomToken);
  250. eventBus.post(new WebSocketCommunicationEvent("roomJoined", joinRoomHashMap));
  251. }
  252. @Override
  253. public void onMessage(WebSocket webSocket, ByteString bytes) {
  254. Log.d(TAG, "Receiving bytes : " + bytes.hex());
  255. }
  256. @Override
  257. public void onClosing(WebSocket webSocket, int code, String reason) {
  258. Log.d(TAG, "Closing : " + code + " / " + reason);
  259. LoggingUtils.writeLogEntryToFile(context,
  260. "WebSocket " + webSocket.hashCode() + " Closing: " + reason);
  261. }
  262. @Override
  263. public void onFailure(WebSocket webSocket, Throwable t, Response response) {
  264. Log.d(TAG, "Error : " + t.getMessage());
  265. LoggingUtils.writeLogEntryToFile(context,
  266. "WebSocket " + webSocket.hashCode() + " onFailure: " + t.getMessage());
  267. closeWebSocket(webSocket);
  268. }
  269. public String getSessionId() {
  270. return sessionId;
  271. }
  272. public boolean hasMCU() {
  273. return hasMCU;
  274. }
  275. public void joinRoomWithRoomTokenAndSession(String roomToken, String normalBackendSession) {
  276. try {
  277. String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken, normalBackendSession));
  278. if (!connected || reconnecting) {
  279. messagesQueue.add(message);
  280. } else {
  281. if (roomToken.equals(currentRoomToken)) {
  282. sendRoomJoinedEvent();
  283. } else {
  284. internalWebSocket.send(message);
  285. }
  286. }
  287. } catch (IOException e) {
  288. e.printStackTrace();
  289. }
  290. }
  291. public void sendCallMessage(NCMessageWrapper ncMessageWrapper) {
  292. try {
  293. String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledCallMessageModel(ncMessageWrapper));
  294. if (!connected || reconnecting) {
  295. messagesQueue.add(message);
  296. } else {
  297. internalWebSocket.send(message);
  298. }
  299. } catch (IOException e) {
  300. LoggingUtils.writeLogEntryToFile(context,
  301. "WebSocket sendCalLMessage: " + e.getMessage() + "\n" + ncMessageWrapper.toString());
  302. Log.e(TAG, "Failed to serialize signaling message");
  303. }
  304. }
  305. public Object getJobWithId(Integer id) {
  306. Object copyJob = magicMap.get(id);
  307. magicMap.remove(id);
  308. return copyJob;
  309. }
  310. public void requestOfferForSessionIdWithType(String sessionIdParam, String roomType) {
  311. try {
  312. String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledRequestOfferModel(sessionIdParam, roomType));
  313. if (!connected || reconnecting) {
  314. messagesQueue.add(message);
  315. } else {
  316. internalWebSocket.send(message);
  317. }
  318. } catch (IOException e) {
  319. LoggingUtils.writeLogEntryToFile(context,
  320. "WebSocket requestOfferForSessionIdWithType: " + e.getMessage() + "\n" + sessionIdParam + " " + roomType);
  321. Log.e(TAG, "Failed to offer request");
  322. }
  323. }
  324. void sendBye() {
  325. if (connected) {
  326. try {
  327. ByeWebSocketMessage byeWebSocketMessage = new ByeWebSocketMessage();
  328. byeWebSocketMessage.setType("bye");
  329. byeWebSocketMessage.setBye(new HashMap<>());
  330. internalWebSocket.send(LoganSquare.serialize(byeWebSocketMessage));
  331. } catch (IOException e) {
  332. Log.e(TAG, "Failed to serialize bye message");
  333. }
  334. }
  335. }
  336. public boolean isConnected() {
  337. return connected;
  338. }
  339. public String getDisplayNameForSession(String session) {
  340. if (usersHashMap.containsKey(session)) {
  341. return usersHashMap.get(session).getDisplayName();
  342. }
  343. return NextcloudTalkApplication.getSharedApplication().getString(R.string.nc_nick_guest);
  344. }
  345. public String getSessionForUserId(String userId) {
  346. for (String session : usersHashMap.keySet()) {
  347. if (userId.equals(usersHashMap.get(session).getUserId())) {
  348. return session;
  349. }
  350. }
  351. return "";
  352. }
  353. public String getUserIdForSession(String session) {
  354. if (usersHashMap.containsKey(session)) {
  355. return usersHashMap.get(session).getUserId();
  356. }
  357. return "";
  358. }
  359. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  360. public void onMessageEvent(NetworkEvent networkEvent) {
  361. if (networkEvent.getNetworkConnectionEvent().equals(NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) && !isConnected()) {
  362. restartWebSocket();
  363. }
  364. }
  365. }