MagicWebSocketInstance.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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 com.bluelinelabs.logansquare.LoganSquare;
  25. import com.nextcloud.talk.R;
  26. import com.nextcloud.talk.application.NextcloudTalkApplication;
  27. import com.nextcloud.talk.events.NetworkEvent;
  28. import com.nextcloud.talk.events.WebSocketCommunicationEvent;
  29. import com.nextcloud.talk.models.database.UserEntity;
  30. import com.nextcloud.talk.models.json.participants.Participant;
  31. import com.nextcloud.talk.models.json.signaling.NCMessageWrapper;
  32. import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
  33. import com.nextcloud.talk.models.json.websocket.BaseWebSocketMessage;
  34. import com.nextcloud.talk.models.json.websocket.ByeWebSocketMessage;
  35. import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage;
  36. import com.nextcloud.talk.models.json.websocket.ErrorOverallWebSocketMessage;
  37. import com.nextcloud.talk.models.json.websocket.EventOverallWebSocketMessage;
  38. import com.nextcloud.talk.models.json.websocket.HelloResponseOverallWebSocketMessage;
  39. import com.nextcloud.talk.models.json.websocket.JoinedRoomOverallWebSocketMessage;
  40. import com.nextcloud.talk.utils.MagicMap;
  41. import com.nextcloud.talk.utils.bundle.BundleKeys;
  42. import org.greenrobot.eventbus.EventBus;
  43. import org.greenrobot.eventbus.Subscribe;
  44. import org.greenrobot.eventbus.ThreadMode;
  45. import java.io.IOException;
  46. import java.util.ArrayList;
  47. import java.util.HashMap;
  48. import java.util.List;
  49. import java.util.Map;
  50. import javax.inject.Inject;
  51. import autodagger.AutoInjector;
  52. import okhttp3.OkHttpClient;
  53. import okhttp3.Request;
  54. import okhttp3.Response;
  55. import okhttp3.WebSocket;
  56. import okhttp3.WebSocketListener;
  57. import okio.ByteString;
  58. import static com.nextcloud.talk.models.json.participants.Participant.ActorType.GUESTS;
  59. import static com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS;
  60. @AutoInjector(NextcloudTalkApplication.class)
  61. public class MagicWebSocketInstance extends WebSocketListener {
  62. private static final String TAG = "MagicWebSocketInstance";
  63. @Inject
  64. OkHttpClient okHttpClient;
  65. @Inject
  66. EventBus eventBus;
  67. @Inject
  68. Context context;
  69. private UserEntity conversationUser;
  70. private String webSocketTicket;
  71. private String resumeId;
  72. private String sessionId;
  73. private boolean hasMCU;
  74. private boolean connected;
  75. private WebSocketConnectionHelper webSocketConnectionHelper;
  76. private WebSocket internalWebSocket;
  77. private MagicMap magicMap;
  78. private String connectionUrl;
  79. private String currentRoomToken;
  80. private int restartCount = 0;
  81. private boolean reconnecting = false;
  82. private HashMap<String, Participant> usersHashMap;
  83. private List<String> messagesQueue = new ArrayList<>();
  84. MagicWebSocketInstance(UserEntity conversationUser, String connectionUrl, String webSocketTicket) {
  85. NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
  86. this.connectionUrl = connectionUrl;
  87. this.conversationUser = conversationUser;
  88. this.webSocketTicket = webSocketTicket;
  89. this.webSocketConnectionHelper = new WebSocketConnectionHelper();
  90. this.usersHashMap = new HashMap<>();
  91. magicMap = new MagicMap();
  92. connected = false;
  93. eventBus.register(this);
  94. restartWebSocket();
  95. }
  96. private void sendHello() {
  97. try {
  98. if (TextUtils.isEmpty(resumeId)) {
  99. internalWebSocket.send(LoganSquare.serialize(webSocketConnectionHelper.getAssembledHelloModel(conversationUser, webSocketTicket)));
  100. } else {
  101. internalWebSocket.send(LoganSquare.serialize(webSocketConnectionHelper.getAssembledHelloModelForResume(resumeId)));
  102. }
  103. } catch (IOException e) {
  104. Log.e(TAG, "Failed to serialize hello model");
  105. }
  106. }
  107. @Override
  108. public void onOpen(WebSocket webSocket, Response response) {
  109. internalWebSocket = webSocket;
  110. sendHello();
  111. }
  112. private void closeWebSocket(WebSocket webSocket) {
  113. webSocket.close(1000, null);
  114. webSocket.cancel();
  115. if (webSocket == internalWebSocket) {
  116. connected = false;
  117. messagesQueue = new ArrayList<>();
  118. }
  119. restartWebSocket();
  120. }
  121. public void clearResumeId() {
  122. resumeId = "";
  123. }
  124. public void restartWebSocket() {
  125. reconnecting = true;
  126. // TODO when improving logging, keep in mind this issue: https://github.com/nextcloud/talk-android/issues/1013
  127. Log.d(TAG, "restartWebSocket: " + connectionUrl);
  128. Request request = new Request.Builder().url(connectionUrl).build();
  129. okHttpClient.newWebSocket(request, this);
  130. restartCount++;
  131. }
  132. @Override
  133. public void onMessage(WebSocket webSocket, String text) {
  134. if (webSocket == internalWebSocket) {
  135. Log.d(TAG, "Receiving : " + webSocket.toString() + " " + text);
  136. try {
  137. BaseWebSocketMessage baseWebSocketMessage = LoganSquare.parse(text, BaseWebSocketMessage.class);
  138. String messageType = baseWebSocketMessage.getType();
  139. switch (messageType) {
  140. case "hello":
  141. connected = true;
  142. reconnecting = false;
  143. restartCount = 0;
  144. String oldResumeId = resumeId;
  145. HelloResponseOverallWebSocketMessage helloResponseWebSocketMessage = LoganSquare.parse(text, HelloResponseOverallWebSocketMessage.class);
  146. resumeId = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getResumeId();
  147. sessionId = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getSessionId();
  148. hasMCU = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().serverHasMCUSupport();
  149. for (int i = 0; i < messagesQueue.size(); i++) {
  150. webSocket.send(messagesQueue.get(i));
  151. }
  152. messagesQueue = new ArrayList<>();
  153. HashMap<String, String> helloHasHap = new HashMap<>();
  154. if (!TextUtils.isEmpty(oldResumeId)) {
  155. helloHasHap.put("oldResumeId", oldResumeId);
  156. } else {
  157. currentRoomToken = "";
  158. }
  159. if (!TextUtils.isEmpty(currentRoomToken)) {
  160. helloHasHap.put("roomToken", currentRoomToken);
  161. }
  162. eventBus.post(new WebSocketCommunicationEvent("hello", helloHasHap));
  163. break;
  164. case "error":
  165. Log.e(TAG, "Received error: " + text);
  166. ErrorOverallWebSocketMessage errorOverallWebSocketMessage = LoganSquare.parse(text, ErrorOverallWebSocketMessage.class);
  167. if (("no_such_session").equals(errorOverallWebSocketMessage.getErrorWebSocketMessage().getCode())) {
  168. Log.d(TAG, "WebSocket " + webSocket.hashCode() + " resumeID " + resumeId + " expired");
  169. resumeId = "";
  170. currentRoomToken = "";
  171. restartWebSocket();
  172. } else if (("hello_expected").equals(errorOverallWebSocketMessage.getErrorWebSocketMessage().getCode())) {
  173. restartWebSocket();
  174. }
  175. break;
  176. case "room":
  177. JoinedRoomOverallWebSocketMessage joinedRoomOverallWebSocketMessage = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage.class);
  178. currentRoomToken = joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage().getRoomId();
  179. if (joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage().getRoomPropertiesWebSocketMessage() != null && !TextUtils.isEmpty(currentRoomToken)) {
  180. sendRoomJoinedEvent();
  181. }
  182. break;
  183. case "event":
  184. EventOverallWebSocketMessage eventOverallWebSocketMessage = LoganSquare.parse(text, EventOverallWebSocketMessage.class);
  185. if (eventOverallWebSocketMessage.getEventMap() != null) {
  186. String target = (String) eventOverallWebSocketMessage.getEventMap().get("target");
  187. switch (target) {
  188. case "room":
  189. if (eventOverallWebSocketMessage.getEventMap().get("type").equals("message")) {
  190. Map<String, Object> messageHashMap =
  191. (Map<String, Object>) eventOverallWebSocketMessage.getEventMap().get("message");
  192. if (messageHashMap.containsKey("data")) {
  193. Map<String, Object> dataHashMap = (Map<String, Object>) messageHashMap.get(
  194. "data");
  195. if (dataHashMap.containsKey("chat")) {
  196. boolean shouldRefreshChat;
  197. Map<String, Object> chatMap = (Map<String, Object>) dataHashMap.get("chat");
  198. if (chatMap.containsKey("refresh")) {
  199. shouldRefreshChat = (boolean) chatMap.get("refresh");
  200. if (shouldRefreshChat) {
  201. HashMap<String, String> refreshChatHashMap = new HashMap<>();
  202. refreshChatHashMap.put(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), (String) messageHashMap.get("roomid"));
  203. refreshChatHashMap.put(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), Long.toString(conversationUser.getId()));
  204. eventBus.post(new WebSocketCommunicationEvent("refreshChat", refreshChatHashMap));
  205. }
  206. }
  207. }
  208. }
  209. } else if (eventOverallWebSocketMessage.getEventMap().get("type").equals("join")) {
  210. List<HashMap<String, Object>> joinEventMap = (List<HashMap<String, Object>>) eventOverallWebSocketMessage.getEventMap().get("join");
  211. HashMap<String, Object> internalHashMap;
  212. Participant participant;
  213. for (int i = 0; i < joinEventMap.size(); i++) {
  214. internalHashMap = joinEventMap.get(i);
  215. HashMap<String, Object> userMap = (HashMap<String, Object>) internalHashMap.get("user");
  216. participant = new Participant();
  217. String userId = (String) internalHashMap.get("userid");
  218. if (userId != null) {
  219. participant.setActorType(USERS);
  220. participant.setActorId(userId);
  221. } else {
  222. participant.setActorType(GUESTS);
  223. // FIXME seems to be not given by the HPB: participant.setActorId();
  224. }
  225. if (userMap != null) {
  226. // There is no "user" attribute for guest participants.
  227. participant.setDisplayName((String) userMap.get("displayname"));
  228. }
  229. usersHashMap.put((String) internalHashMap.get("sessionid"), participant);
  230. }
  231. }
  232. break;
  233. case "participants":
  234. if (eventOverallWebSocketMessage.getEventMap().get("type").equals("update")) {
  235. HashMap<String, String> refreshChatHashMap = new HashMap<>();
  236. HashMap<String, Object> updateEventMap = (HashMap<String, Object>) eventOverallWebSocketMessage.getEventMap().get("update");
  237. refreshChatHashMap.put("roomToken", (String) updateEventMap.get("roomid"));
  238. refreshChatHashMap.put("jobId", Integer.toString(magicMap.add(updateEventMap.get("users"))));
  239. eventBus.post(new WebSocketCommunicationEvent("participantsUpdate", refreshChatHashMap));
  240. }
  241. break;
  242. }
  243. }
  244. break;
  245. case "message":
  246. CallOverallWebSocketMessage callOverallWebSocketMessage = LoganSquare.parse(text, CallOverallWebSocketMessage.class);
  247. NCSignalingMessage ncSignalingMessage = callOverallWebSocketMessage.getCallWebSocketMessage().getNcSignalingMessage();
  248. if (TextUtils.isEmpty(ncSignalingMessage.getFrom()) && callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage() != null) {
  249. ncSignalingMessage.setFrom(callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage().getSessionId());
  250. }
  251. if (!TextUtils.isEmpty(ncSignalingMessage.getFrom())) {
  252. HashMap<String, String> messageHashMap = new HashMap<>();
  253. messageHashMap.put("jobId", Integer.toString(magicMap.add(ncSignalingMessage)));
  254. eventBus.post(new WebSocketCommunicationEvent("signalingMessage", messageHashMap));
  255. }
  256. break;
  257. case "bye":
  258. connected = false;
  259. resumeId = "";
  260. default:
  261. break;
  262. }
  263. } catch (IOException e) {
  264. Log.e(TAG, "Failed to recognize WebSocket message", e);
  265. }
  266. }
  267. }
  268. private void sendRoomJoinedEvent() {
  269. HashMap<String, String> joinRoomHashMap = new HashMap<>();
  270. joinRoomHashMap.put("roomToken", currentRoomToken);
  271. eventBus.post(new WebSocketCommunicationEvent("roomJoined", joinRoomHashMap));
  272. }
  273. @Override
  274. public void onMessage(WebSocket webSocket, ByteString bytes) {
  275. Log.d(TAG, "Receiving bytes : " + bytes.hex());
  276. }
  277. @Override
  278. public void onClosing(WebSocket webSocket, int code, String reason) {
  279. Log.d(TAG, "Closing : " + code + " / " + reason);
  280. }
  281. @Override
  282. public void onFailure(WebSocket webSocket, Throwable t, Response response) {
  283. Log.d(TAG, "Error : WebSocket " + webSocket.hashCode() + " onFailure: " + t.getMessage());
  284. closeWebSocket(webSocket);
  285. }
  286. public String getSessionId() {
  287. return sessionId;
  288. }
  289. public boolean hasMCU() {
  290. return hasMCU;
  291. }
  292. public void joinRoomWithRoomTokenAndSession(String roomToken, String normalBackendSession) {
  293. Log.d(TAG, "joinRoomWithRoomTokenAndSession");
  294. Log.d(TAG, " roomToken: " + roomToken);
  295. Log.d(TAG, " session: " + normalBackendSession);
  296. try {
  297. String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken, normalBackendSession));
  298. if (!connected || reconnecting) {
  299. messagesQueue.add(message);
  300. } else {
  301. if (roomToken.equals(currentRoomToken)) {
  302. sendRoomJoinedEvent();
  303. } else {
  304. internalWebSocket.send(message);
  305. }
  306. }
  307. } catch (IOException e) {
  308. Log.e(TAG, e.getMessage(), e);
  309. }
  310. }
  311. public void sendCallMessage(NCMessageWrapper ncMessageWrapper) {
  312. try {
  313. String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledCallMessageModel(ncMessageWrapper));
  314. if (!connected || reconnecting) {
  315. messagesQueue.add(message);
  316. } else {
  317. internalWebSocket.send(message);
  318. }
  319. } catch (IOException e) {
  320. Log.e(TAG, "Failed to serialize signaling message", e);
  321. }
  322. }
  323. public Object getJobWithId(Integer id) {
  324. Object copyJob = magicMap.get(id);
  325. magicMap.remove(id);
  326. return copyJob;
  327. }
  328. public void requestOfferForSessionIdWithType(String sessionIdParam, String roomType) {
  329. try {
  330. String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledRequestOfferModel(sessionIdParam, roomType));
  331. if (!connected || reconnecting) {
  332. messagesQueue.add(message);
  333. } else {
  334. internalWebSocket.send(message);
  335. }
  336. } catch (IOException e) {
  337. Log.e(TAG, "Failed to offer request. sessionIdParam: " + sessionIdParam + " roomType:" + roomType, e);
  338. }
  339. }
  340. void sendBye() {
  341. if (connected) {
  342. try {
  343. ByeWebSocketMessage byeWebSocketMessage = new ByeWebSocketMessage();
  344. byeWebSocketMessage.setType("bye");
  345. byeWebSocketMessage.setBye(new HashMap<>());
  346. internalWebSocket.send(LoganSquare.serialize(byeWebSocketMessage));
  347. } catch (IOException e) {
  348. Log.e(TAG, "Failed to serialize bye message");
  349. }
  350. }
  351. }
  352. public boolean isConnected() {
  353. return connected;
  354. }
  355. public String getDisplayNameForSession(String session) {
  356. Participant participant = usersHashMap.get(session);
  357. if (participant != null) {
  358. if (participant.getDisplayName() != null) {
  359. return participant.getDisplayName();
  360. }
  361. }
  362. return NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_nick_guest);
  363. }
  364. public String getUserIdForSession(String session) {
  365. Participant participant = usersHashMap.get(session);
  366. if (participant != null) {
  367. if (participant.getActorType() == USERS) {
  368. return participant.getActorId();
  369. }
  370. }
  371. return "";
  372. }
  373. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  374. public void onMessageEvent(NetworkEvent networkEvent) {
  375. if (networkEvent.getNetworkConnectionEvent().equals(NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) && !isConnected()) {
  376. restartWebSocket();
  377. }
  378. }
  379. }