123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- /*
- * 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 com.bluelinelabs.logansquare.LoganSquare;
- import com.nextcloud.talk.R;
- import com.nextcloud.talk.application.NextcloudTalkApplication;
- import com.nextcloud.talk.data.user.model.User;
- import com.nextcloud.talk.events.NetworkEvent;
- 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.signaling.NCSignalingMessage;
- 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.utils.MagicMap;
- import com.nextcloud.talk.utils.bundle.BundleKeys;
- import org.greenrobot.eventbus.EventBus;
- import org.greenrobot.eventbus.Subscribe;
- import org.greenrobot.eventbus.ThreadMode;
- import java.io.IOException;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import javax.inject.Inject;
- import autodagger.AutoInjector;
- import okhttp3.OkHttpClient;
- import okhttp3.Request;
- import okhttp3.Response;
- import okhttp3.WebSocket;
- import okhttp3.WebSocketListener;
- import okio.ByteString;
- import static com.nextcloud.talk.models.json.participants.Participant.ActorType.GUESTS;
- import static com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS;
- import static com.nextcloud.talk.webrtc.Globals.EVENT_TYPE;
- import static com.nextcloud.talk.webrtc.Globals.EVENT_TYPE_UPDATE;
- import static com.nextcloud.talk.webrtc.Globals.JOB_ID;
- import static com.nextcloud.talk.webrtc.Globals.PARTICIPANTS_UPDATE;
- import static com.nextcloud.talk.webrtc.Globals.ROOM_TOKEN;
- import static com.nextcloud.talk.webrtc.Globals.TARGET_PARTICIPANTS;
- import static com.nextcloud.talk.webrtc.Globals.UPDATE_ALL;
- import static com.nextcloud.talk.webrtc.Globals.UPDATE_IN_CALL;
- import static com.nextcloud.talk.webrtc.Globals.UPDATE_ROOM_ID;
- import static com.nextcloud.talk.webrtc.Globals.UPDATE_USERS;
- @AutoInjector(NextcloudTalkApplication.class)
- public class MagicWebSocketInstance extends WebSocketListener {
- private static final String TAG = "MagicWebSocketInstance";
- @Inject
- OkHttpClient okHttpClient;
- @Inject
- EventBus eventBus;
- @Inject
- Context context;
- private User conversationUser;
- private String webSocketTicket;
- private String resumeId;
- private String sessionId;
- private boolean hasMCU;
- private boolean connected;
- private WebSocketConnectionHelper webSocketConnectionHelper;
- private WebSocket internalWebSocket;
- private MagicMap magicMap;
- private String connectionUrl;
- private String currentRoomToken;
- private int restartCount = 0;
- private boolean reconnecting = false;
- private HashMap<String, Participant> usersHashMap;
- private List<String> messagesQueue = new ArrayList<>();
- MagicWebSocketInstance(User conversationUser, String connectionUrl, String webSocketTicket) {
- NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
- this.connectionUrl = connectionUrl;
- this.conversationUser = conversationUser;
- this.webSocketTicket = webSocketTicket;
- this.webSocketConnectionHelper = new WebSocketConnectionHelper();
- this.usersHashMap = new HashMap<>();
- magicMap = new MagicMap();
- connected = false;
- eventBus.register(this);
- restartWebSocket();
- }
- private void sendHello() {
- try {
- if (TextUtils.isEmpty(resumeId)) {
- internalWebSocket.send(LoganSquare.serialize(webSocketConnectionHelper.getAssembledHelloModel(conversationUser, webSocketTicket)));
- } else {
- internalWebSocket.send(LoganSquare.serialize(webSocketConnectionHelper.getAssembledHelloModelForResume(resumeId)));
- }
- } catch (IOException e) {
- Log.e(TAG, "Failed to serialize hello model");
- }
- }
- @Override
- public void onOpen(WebSocket webSocket, Response response) {
- internalWebSocket = webSocket;
- sendHello();
- }
- private void closeWebSocket(WebSocket webSocket) {
- webSocket.close(1000, null);
- webSocket.cancel();
- if (webSocket == internalWebSocket) {
- connected = false;
- messagesQueue = new ArrayList<>();
- }
- restartWebSocket();
- }
- public void clearResumeId() {
- resumeId = "";
- }
- public void restartWebSocket() {
- reconnecting = true;
- // TODO when improving logging, keep in mind this issue: https://github.com/nextcloud/talk-android/issues/1013
- Log.d(TAG, "restartWebSocket: " + connectionUrl);
- Request request = new Request.Builder().url(connectionUrl).build();
- okHttpClient.newWebSocket(request, this);
- restartCount++;
- }
- @Override
- public void onMessage(WebSocket webSocket, String text) {
- if (webSocket == internalWebSocket) {
- Log.d(TAG, "Receiving : " + webSocket.toString() + " " + text);
- try {
- BaseWebSocketMessage baseWebSocketMessage = LoganSquare.parse(text, BaseWebSocketMessage.class);
- String messageType = baseWebSocketMessage.getType();
- switch (messageType) {
- case "hello":
- connected = true;
- reconnecting = false;
- restartCount = 0;
- String oldResumeId = resumeId;
- HelloResponseOverallWebSocketMessage helloResponseWebSocketMessage = LoganSquare.parse(text, HelloResponseOverallWebSocketMessage.class);
- resumeId = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getResumeId();
- sessionId = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getSessionId();
- hasMCU = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().serverHasMCUSupport();
- for (int i = 0; i < messagesQueue.size(); i++) {
- webSocket.send(messagesQueue.get(i));
- }
- messagesQueue = new ArrayList<>();
- HashMap<String, String> helloHasHap = new HashMap<>();
- if (!TextUtils.isEmpty(oldResumeId)) {
- helloHasHap.put("oldResumeId", oldResumeId);
- } else {
- currentRoomToken = "";
- }
- if (!TextUtils.isEmpty(currentRoomToken)) {
- helloHasHap.put(ROOM_TOKEN, currentRoomToken);
- }
- eventBus.post(new WebSocketCommunicationEvent("hello", helloHasHap));
- break;
- case "error":
- Log.e(TAG, "Received error: " + text);
- ErrorOverallWebSocketMessage errorOverallWebSocketMessage = LoganSquare.parse(text, ErrorOverallWebSocketMessage.class);
- if (("no_such_session").equals(errorOverallWebSocketMessage.getErrorWebSocketMessage().getCode())) {
- Log.d(TAG, "WebSocket " + webSocket.hashCode() + " resumeID " + resumeId + " expired");
- resumeId = "";
- currentRoomToken = "";
- restartWebSocket();
- } else if (("hello_expected").equals(errorOverallWebSocketMessage.getErrorWebSocketMessage().getCode())) {
- restartWebSocket();
- }
- break;
- case "room":
- JoinedRoomOverallWebSocketMessage joinedRoomOverallWebSocketMessage = LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage.class);
- currentRoomToken = joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage().getRoomId();
- if (joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage().getRoomPropertiesWebSocketMessage() != null && !TextUtils.isEmpty(currentRoomToken)) {
- sendRoomJoinedEvent();
- }
- break;
- case "event":
- EventOverallWebSocketMessage eventOverallWebSocketMessage = LoganSquare.parse(text, EventOverallWebSocketMessage.class);
- if (eventOverallWebSocketMessage.getEventMap() != null) {
- String target = (String) eventOverallWebSocketMessage.getEventMap().get("target");
- switch (target) {
- case "room":
- if (eventOverallWebSocketMessage.getEventMap().get("type").equals("message")) {
- Map<String, Object> messageHashMap =
- (Map<String, Object>) eventOverallWebSocketMessage.getEventMap().get("message");
- if (messageHashMap.containsKey("data")) {
- Map<String, Object> dataHashMap = (Map<String, Object>) messageHashMap.get(
- "data");
- if (dataHashMap.containsKey("chat")) {
- boolean shouldRefreshChat;
- Map<String, Object> chatMap = (Map<String, Object>) dataHashMap.get("chat");
- if (chatMap.containsKey("refresh")) {
- shouldRefreshChat = (boolean) chatMap.get("refresh");
- if (shouldRefreshChat) {
- HashMap<String, String> refreshChatHashMap = new HashMap<>();
- refreshChatHashMap.put(BundleKeys.KEY_ROOM_TOKEN, (String) messageHashMap.get("roomid"));
- refreshChatHashMap.put(BundleKeys.KEY_INTERNAL_USER_ID, Long.toString(conversationUser.getId()));
- eventBus.post(new WebSocketCommunicationEvent("refreshChat", refreshChatHashMap));
- }
- }
- }
- }
- } else if (eventOverallWebSocketMessage.getEventMap().get("type").equals("join")) {
- List<HashMap<String, Object>> joinEventList = (List<HashMap<String, Object>>) eventOverallWebSocketMessage.getEventMap().get("join");
- HashMap<String, Object> internalHashMap;
- Participant participant;
- for (int i = 0; i < joinEventList.size(); i++) {
- internalHashMap = joinEventList.get(i);
- HashMap<String, Object> userMap = (HashMap<String, Object>) internalHashMap.get("user");
- participant = new Participant();
- String userId = (String) internalHashMap.get("userid");
- if (userId != null) {
- participant.setActorType(USERS);
- participant.setActorId(userId);
- } else {
- participant.setActorType(GUESTS);
- // FIXME seems to be not given by the HPB: participant.setActorId();
- }
- if (userMap != null) {
- // There is no "user" attribute for guest participants.
- participant.setDisplayName((String) userMap.get("displayname"));
- }
- usersHashMap.put((String) internalHashMap.get("sessionid"), participant);
- }
- }
- break;
- case TARGET_PARTICIPANTS:
- if (EVENT_TYPE_UPDATE.equals(eventOverallWebSocketMessage.getEventMap().get(EVENT_TYPE))) {
- HashMap<String, String> refreshChatHashMap = new HashMap<>();
- HashMap<String, Object> updateEventMap = (HashMap<String, Object>) eventOverallWebSocketMessage.getEventMap().get(EVENT_TYPE_UPDATE);
- if (updateEventMap == null) {
- break;
- }
- if (updateEventMap.containsKey(UPDATE_ROOM_ID)) {
- Object updateRoomId = updateEventMap.get(UPDATE_ROOM_ID);
- if (updateRoomId != null) {
- refreshChatHashMap.put(ROOM_TOKEN,
- (String) updateEventMap.get(UPDATE_ROOM_ID));
- }
- }
- if (updateEventMap.containsKey(UPDATE_USERS)) {
- Object updateUsers = updateEventMap.get(UPDATE_USERS);
- if (updateUsers != null) {
- refreshChatHashMap.put(JOB_ID, Integer.toString(magicMap.add(updateUsers)));
- }
- }
- if (updateEventMap.containsKey(UPDATE_IN_CALL)) {
- Object inCall = updateEventMap.get(UPDATE_IN_CALL);
- if (inCall != null) {
- refreshChatHashMap.put(UPDATE_IN_CALL, Long.toString((Long) inCall));
- }
- }
- if (updateEventMap.containsKey(UPDATE_ALL)) {
- Object updateAll = updateEventMap.get(UPDATE_ALL);
- if (updateAll != null) {
- refreshChatHashMap.put(UPDATE_ALL, Boolean.toString((Boolean) updateAll));
- }
- }
- eventBus.post(new WebSocketCommunicationEvent(PARTICIPANTS_UPDATE, refreshChatHashMap));
- }
- break;
- }
- }
- break;
- case "message":
- CallOverallWebSocketMessage callOverallWebSocketMessage = LoganSquare.parse(text, CallOverallWebSocketMessage.class);
- NCSignalingMessage ncSignalingMessage = callOverallWebSocketMessage.getCallWebSocketMessage().getNcSignalingMessage();
- if (TextUtils.isEmpty(ncSignalingMessage.getFrom()) && callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage() != null) {
- ncSignalingMessage.setFrom(callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage().getSessionId());
- }
- if (!TextUtils.isEmpty(ncSignalingMessage.getFrom())) {
- HashMap<String, String> messageHashMap = new HashMap<>();
- messageHashMap.put(JOB_ID, Integer.toString(magicMap.add(ncSignalingMessage)));
- eventBus.post(new WebSocketCommunicationEvent("signalingMessage", messageHashMap));
- }
- break;
- case "bye":
- connected = false;
- resumeId = "";
- default:
- break;
- }
- } catch (IOException e) {
- Log.e(TAG, "Failed to recognize WebSocket message", e);
- }
- }
- }
- private void sendRoomJoinedEvent() {
- HashMap<String, String> joinRoomHashMap = new HashMap<>();
- joinRoomHashMap.put(ROOM_TOKEN, currentRoomToken);
- eventBus.post(new WebSocketCommunicationEvent("roomJoined", joinRoomHashMap));
- }
- @Override
- public void onMessage(WebSocket webSocket, ByteString bytes) {
- Log.d(TAG, "Receiving bytes : " + bytes.hex());
- }
- @Override
- public void onClosing(WebSocket webSocket, int code, String reason) {
- Log.d(TAG, "Closing : " + code + " / " + reason);
- }
- @Override
- public void onFailure(WebSocket webSocket, Throwable t, Response response) {
- Log.d(TAG, "Error : WebSocket " + webSocket.hashCode() + " onFailure: " + t.getMessage());
- closeWebSocket(webSocket);
- }
- public String getSessionId() {
- return sessionId;
- }
- public boolean hasMCU() {
- return hasMCU;
- }
- public void joinRoomWithRoomTokenAndSession(String roomToken, String normalBackendSession) {
- Log.d(TAG, "joinRoomWithRoomTokenAndSession");
- Log.d(TAG, " roomToken: " + roomToken);
- Log.d(TAG, " session: " + normalBackendSession);
- try {
- String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken, normalBackendSession));
- if (!connected || reconnecting) {
- messagesQueue.add(message);
- } else {
- if (roomToken.equals(currentRoomToken)) {
- sendRoomJoinedEvent();
- } else {
- internalWebSocket.send(message);
- }
- }
- } catch (IOException e) {
- Log.e(TAG, e.getMessage(), e);
- }
- }
- public void sendCallMessage(NCMessageWrapper ncMessageWrapper) {
- try {
- String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledCallMessageModel(ncMessageWrapper));
- if (!connected || reconnecting) {
- messagesQueue.add(message);
- } else {
- internalWebSocket.send(message);
- }
- } catch (IOException e) {
- Log.e(TAG, "Failed to serialize signaling message", e);
- }
- }
- public Object getJobWithId(Integer id) {
- Object copyJob = magicMap.get(id);
- magicMap.remove(id);
- return copyJob;
- }
- public void requestOfferForSessionIdWithType(String sessionIdParam, String roomType) {
- try {
- String message = LoganSquare.serialize(webSocketConnectionHelper.getAssembledRequestOfferModel(sessionIdParam, roomType));
- if (!connected || reconnecting) {
- messagesQueue.add(message);
- } else {
- internalWebSocket.send(message);
- }
- } catch (IOException e) {
- Log.e(TAG, "Failed to offer request. sessionIdParam: " + sessionIdParam + " roomType:" + roomType, e);
- }
- }
- void sendBye() {
- if (connected) {
- try {
- ByeWebSocketMessage byeWebSocketMessage = new ByeWebSocketMessage();
- byeWebSocketMessage.setType("bye");
- byeWebSocketMessage.setBye(new HashMap<>());
- internalWebSocket.send(LoganSquare.serialize(byeWebSocketMessage));
- } catch (IOException e) {
- Log.e(TAG, "Failed to serialize bye message");
- }
- }
- }
- public boolean isConnected() {
- return connected;
- }
- public String getDisplayNameForSession(String session) {
- Participant participant = usersHashMap.get(session);
- if (participant != null) {
- if (participant.getDisplayName() != null) {
- return participant.getDisplayName();
- }
- }
- return NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_nick_guest);
- }
- public String getUserIdForSession(String session) {
- Participant participant = usersHashMap.get(session);
- if (participant != null) {
- if (participant.getCalculatedActorType() == USERS) {
- return participant.getCalculatedActorId();
- }
- }
- return "";
- }
- @Subscribe(threadMode = ThreadMode.BACKGROUND)
- public void onMessageEvent(NetworkEvent networkEvent) {
- if (networkEvent.getNetworkConnectionEvent() == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED && !isConnected()) {
- restartWebSocket();
- }
- }
- }
|