MagicWebSocketInstance.java 22 KB

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