/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2015, Telestax Inc and individual contributors * by the @authors tag. * * This program is free software: you can redistribute it and/or modify * under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * * For questions related to commercial use licensing, please contact sales@telestax.com. * */ package org.restcomm.android.sdk.SignalingClient; import android.content.Context; import android.os.Handler; import android.os.Message; import org.restcomm.android.sdk.RCClient; import org.restcomm.android.sdk.RCDeviceListener; //import org.restcomm.android.sdk.RCMessage; import org.restcomm.android.sdk.util.RCLogger; import java.util.HashMap; /** * SignalingClient is a singleton that provides asynchronous access to lower level signaling facilities. Requests are sent via methods * like open(), close(), etc towards signaling thread. Responses are received via Handler.handleMessage() from signaling thread * and sent for further processing to SignalingClientListener listener for register/configuration specific functionality and to * SignalingClientCallListener listener for call related functionality. Hence, users of this API should implement those * and properly handle responses and events. * * Each request towards the signaling thread is sent together with a unique jobId that identifies this 'job' until it is either complete * or an error occurs. Each reply/event associated with this request carries the same jobId, so that the App can correlate them. The lifetime * of the job depends on its type. For example for open() jobs it is until the signaling facilities are properly initialized or we get an error * at which point we get a onCloseReply() conveying either of the facts. For a call() job the jobId is sent back and forth until the call * is disconnected or an error occurs. * * Notice that some callbacks are called on*Reply() (as opposed to on*Event()). The convention is that those are directly associated * to a a simple request (typically non call related) and also carry error status (and possibly connectivity status). In contrast, * for call-related functionality separate callbacks are defined for a. success and b. for error codes, since they are much more complicated. * * The whole architecture in a nutshell is as follows. The Android App or a higher level API (like RCDevice/RCConnection) use the SignalingClient API * which underneath creates signaling messages (see SignalingMessage for structure of the messages) and sends them to the Signaling thread * that handles them by dispatching them to JainSipClient that encapsulates JAIN SIP. When a response/event comes in from JAIN SIP to JainSipClient * the reverse happens: a message is created from the Signaling thread to the UI thread and after it is received at handleMessage() the respective * listener callback is used to notify the UI. */ public class SignalingClient extends Handler { /** * Registration/configuration related interface callbacks that user of the API needs to implement */ public interface SignalingClientListener { // Replies void onOpenReply(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus, RCClient.ErrorCodes status, String text); void onCloseReply(String jobId, RCClient.ErrorCodes status, String text); void onReconfigureReply(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus, RCClient.ErrorCodes status, String text); void onMessageReply(String jobId, RCClient.ErrorCodes status, String text); // Unsolicited Events void onCallArrivedEvent(String jobId, String peer, String sdpOffer, HashMap<String, String> customHeaders); void onMessageArrivedEvent(String jobId, String peer, String messageText); void onErrorEvent(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus, RCClient.ErrorCodes status, String text); void onConnectivityEvent(String jobId, RCDeviceListener.RCConnectivityStatus connectivityStatus); // Event to convey trying to Register, so that UI can convey that to user (typically by changing RCDevice state to Offline, until register response arrives) void onRegisteringEvent(String jobId); // TODO: this should be removed after we remodel the whole connection/device communication // Call related events that are delegated to RCConnection //void onCallRelatedMessage(SignalingMessage message); // this is not a callback but we want the listener to implement it so that we cat retrieve the connection from the jobId SignalingClient.SignalingClientCallListener getConnectionByJobId(String jobId); } /** * Call related interface callbacks that user of the API needs to implement */ public interface SignalingClientCallListener { void onCallOutgoingConnectedEvent(String jobId, String sdpAnswer, HashMap<String, String> customHeaders); void onCallIncomingConnectedEvent(String jobId); // peer disconnected the call void onCallPeerDisconnectEvent(String jobId); // peer ringing for outgoing call void onCallOutgoingPeerRingingEvent(String jobId); // call was disconnected due to local disconnect() call void onCallLocalDisconnectedEvent(String jobId); void onCallErrorEvent(String jobId, RCClient.ErrorCodes status, String text); // cancel was was answered for incoming call void onCallIncomingCanceledEvent(String jobId); void onCallSentDigitsEvent(String jobId, RCClient.ErrorCodes statusCode, String statusText); } // ------ Not used yet, we 'll use it when we introduce the new messaging API public interface UIMessageListener { void onMessageSentEvent(String jobId); } private static final SignalingClient instance = new SignalingClient(); SignalingClientListener listener; private static final String TAG = "SignalingClient"; // handler at signaling thread to send messages to SignalingHandlerThread signalingHandlerThread; Handler signalingHandler; //UIHandler uiHandler; Context context; //HashMap<String, RCMessage> messages; // private constructor to avoid client applications to use constructor private SignalingClient() { super(); // create signaling handler thread and handler/signal signalingHandlerThread = new SignalingHandlerThread(this); signalingHandler = signalingHandlerThread.getHandler(); } public static SignalingClient getInstance() { return instance; } /** * Initialize the signaling facilities * @param listener Listener to register/configuration specific events * @param context Android context needed by signaling thread * @param parameters A map of parameters of the open (TODO: add doc for specific keys) * @return The jobId for the new job created at Signaling thread */ public String open(SignalingClientListener listener, Context context, HashMap<String, Object> parameters) { //uiHandler = new UIHandler(listener); this.context = context; this.listener = listener; String jobId = generateId(); SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.OPEN_REQUEST); signalingMessage.setParameters(parameters); signalingMessage.setAndroidContext(context); Message message = signalingHandler.obtainMessage(1, signalingMessage); message.sendToTarget(); return jobId; } /** * Change signaling configuration, like update username/password, change domain, etc * @param parameters Reconfigure paramemeters * @return The jobId for the new job created at Signaling thread */ public String reconfigure(HashMap<String, Object> parameters) { String jobId = generateId(); SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.RECONFIGURE_REQUEST); signalingMessage.setParameters(parameters); Message message = signalingHandler.obtainMessage(1, signalingMessage); message.sendToTarget(); return jobId; } // -- Call related methods. For these the jobId is already generated by the application /** * Make a call towards a peer * @param jobId Unique identifier to identify future replies & events * @param parameters Call parameters */ public void call(String jobId, HashMap<String, Object> parameters) { SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.CALL_REQUEST); signalingMessage.setParameters(parameters); Message message = signalingHandler.obtainMessage(1, signalingMessage); message.sendToTarget(); } /** * Accept a call from a peer * @param jobId Unique identifier to identify future replies & events * @param parameters Accept parameters */ public void accept(String jobId, HashMap<String, Object> parameters) { SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.CALL_ACCEPT_REQUEST); signalingMessage.setParameters(parameters); Message message = signalingHandler.obtainMessage(1, signalingMessage); message.sendToTarget(); } /** * Disconnect a call with a pper * @param jobId Unique identifier to identify future replies & events * @param reason Reason for the disconnect. If this is a normal disconnect triggered by the user, this is null or empty. But if this is caused because media * connectivity has been severed, then 'reason' conveys the reason and is added as a SIP header to the generated BYE. */ public void disconnect(String jobId, String reason) { SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.CALL_DISCONNECT_REQUEST); signalingMessage.reason = reason; Message message = signalingHandler.obtainMessage(1, signalingMessage); message.sendToTarget(); } /** * Send DTMF digits to peer over existing call * @param jobId Unique identifier to identify future replies & events * @param digits DTMF digits to send (Important: for now we only support a single digit per sendDigits() call) */ public void sendDigits(String jobId, String digits) { SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.CALL_SEND_DIGITS_REQUEST); signalingMessage.dtmfDigits = digits; Message message = signalingHandler.obtainMessage(1, signalingMessage); message.sendToTarget(); } /** * Send text message to peer * @param parameters * @return */ public String sendMessage(HashMap<String, Object> parameters) { String jobId = generateId(); SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.MESSAGE_REQUEST); signalingMessage.parameters = parameters; Message message = signalingHandler.obtainMessage(1, signalingMessage); message.sendToTarget(); //messages.put(jobId, rcMessage); return jobId; } /** * Release the signaling facilities * @return */ public String close() { String jobId = generateId(); SignalingMessage signalingMessage = new SignalingMessage(jobId, SignalingMessage.MessageType.CLOSE_REQUEST); //signalingMessage.setParameters(parameters); Message message = signalingHandler.obtainMessage(1, signalingMessage); message.sendToTarget(); return jobId; } /** * Handle incoming messages from signaling thread * * @param inputMessage incoming signaling message */ @Override public void handleMessage(Message inputMessage) { // Gets the image task from the incoming Message object. SignalingMessage message = (SignalingMessage) inputMessage.obj; RCLogger.i(TAG, "handleMessage: type: " + message.type + ", jobId: " + message.jobId); if (message.type == SignalingMessage.MessageType.OPEN_REPLY) { listener.onOpenReply(message.jobId, message.connectivityStatus, message.status, message.text); } else if (message.type == SignalingMessage.MessageType.CLOSE_REPLY) { listener.onCloseReply(message.jobId, message.status, message.text); } else if (message.type == SignalingMessage.MessageType.RECONFIGURE_REPLY) { listener.onReconfigureReply(message.jobId, message.connectivityStatus, message.status, message.text); } else if (message.type == SignalingMessage.MessageType.ERROR_EVENT) { listener.onErrorEvent(message.jobId, message.connectivityStatus, message.status, message.text); } else if (message.type == SignalingMessage.MessageType.CONNECTIVITY_EVENT) { listener.onConnectivityEvent(message.jobId, message.connectivityStatus); } else if (message.type == SignalingMessage.MessageType.MESSAGE_INCOMING_EVENT) { listener.onMessageArrivedEvent(message.jobId, message.peer, message.messageText); } else if (message.type == SignalingMessage.MessageType.MESSAGE_REPLY) { /* RCMessage rcMessage = messages.get(message.jobId); if (rcMessage == null) { throw new RuntimeException("No RCMessage matching incoming message jobId: " + message.jobId); } rcMessage.onMessageSentEvent(message.jobId); */ listener.onMessageReply(message.jobId, message.status, message.text); } else if (message.type == SignalingMessage.MessageType.REGISTERING_EVENT) { listener.onRegisteringEvent(message.jobId); } // Call related events else if (message.type == SignalingMessage.MessageType.CALL_INCOMING_EVENT) { listener.onCallArrivedEvent(message.jobId, message.peer, message.sdp, message.customHeaders); } else if (message.type == SignalingMessage.MessageType.CALL_OUTGOING_CONNECTED_EVENT) { SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId); // outgoing call is connected (got 200 OK and ACKed it) callListener.onCallOutgoingConnectedEvent(message.jobId, message.sdp, message.customHeaders); } else if (message.type == SignalingMessage.MessageType.CALL_INCOMING_CONNECTED_EVENT) { // incoming call connected SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId); callListener.onCallIncomingConnectedEvent(message.jobId); } else if (message.type == SignalingMessage.MessageType.CALL_PEER_DISCONNECT_EVENT) { SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId); callListener.onCallPeerDisconnectEvent(message.jobId); } else if (message.type == SignalingMessage.MessageType.CALL_OUTGOING_PEER_RINGING_EVENT) { SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId); callListener.onCallOutgoingPeerRingingEvent(message.jobId); } else if (message.type == SignalingMessage.MessageType.CALL_LOCAL_DISCONNECT_EVENT) { SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId); callListener.onCallLocalDisconnectedEvent(message.jobId); } else if (message.type == SignalingMessage.MessageType.CALL_ERROR_EVENT) { SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId); callListener.onCallErrorEvent(message.jobId, message.status, message.text); } else if (message.type == SignalingMessage.MessageType.CALL_INCOMING_CANCELED_EVENT) { SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId); callListener.onCallIncomingCanceledEvent(message.jobId); } else if (message.type == SignalingMessage.MessageType.CALL_SEND_DIGITS_EVENT) { SignalingClientCallListener callListener = listener.getConnectionByJobId(message.jobId); callListener.onCallSentDigitsEvent(message.jobId, message.status, message.text); } else { RCLogger.e(TAG, "handleSignalingMessage(): no handler for signaling message"); } } // ------ Helpers // Generate unique identifier for 'transactions' created by SignalingClient, this can then be used as call-id when it enters JAIN SIP private String generateId() { return Long.toString(System.currentTimeMillis()); } }