/*
* 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.JainSipClient;
import android.gov.nist.javax.sip.ResponseEventExt;
import android.javax.sip.Transaction;
import org.restcomm.android.sdk.RCClient;
import org.restcomm.android.sdk.RCDevice;
import org.restcomm.android.sdk.RCDeviceListener;
import org.restcomm.android.sdk.util.RCLogger;
import java.util.Arrays;
import java.util.HashMap;
/**
* JainSipJob represents the context of a signaling action until it is either finished or an error occurs. All signaling actions MUST be started as jobs, because
* jobs keep the context that binds requests & responses together. An important note is that not all jobs need an FSM, but whether an FSM is started or not
* for a job depends on JainSipJob.hasFsm()
*
* A JainSipJob mainly holds the following information:
* - jobId: that uniquely identifies a job within the signaling facilities. This typically is a unix timestamp (including milis)
* for outgoing requests provided in the request by the App, and the SIP Call-Id for incoming requests
* - transaction: the SIP Transaction object associated with the job at this point in time. Remember that a job might consist of multiple transactions, hence
* this field might be updated during the job's lifetime
* - parameters: a HashMap holding an arbitrary number of parameters applicable to the job. These can originate at the App, or they can be updated during the course
* of job execution
*/
class JainSipJob {
public enum FsmStates {
START_BIND_REGISTER,
AUTH,
NOTIFY,
REGISTER,
UNREGISTER,
SHUTDOWN,
AUTH_1,
AUTH_2,
UNBIND_BIND_REGISTER,
BIND_REGISTER,
}
public enum FsmEvents {
NONE,
TIMEOUT,
AUTH_REQUIRED,
REGISTER_FAILURE,
REGISTER_SUCCESS,
}
/**
* Some signaling jobs (important: not all jobs have an FSM) are also associated with a state machine to be able to properly address invoking
* the same functionalities in different job contexts without losing track and at the same time notifying the correct UI entities of job status.
*
* The idea here is that you initialize a job and based on its type (i.e. TYPE_OPEN, etc) we have a set of states (i.e. FsmStates) that this jobs typically needs
* to go through. For example job of type TYPE_OPEN needs to start the signaling stack, bind to networking facilities, register, authorize and finally
* notify the App. These steps are reflected in the states member variable and initialized when the job is constructed.
*
* Once the FSM is initialized we can then call JainSipFsm.process() that will do the actual FSM processing based on current state (i.e. states variable) and the event (i.e. FsmEvents)
* passed by the caller (check JainSipFsm.process())
*
* It's important to avoid calling any methods within JainSipFsm.process() that inside them call JainSipFsm.process(), cause that would probably cause corruption
* of the FSM state
*
* Some areas that need further improvement at some point:
* - JainSipFsm.process() code has become spaghetti so we need to consider avoiding code duplication
* - Right now we use JainSipJob.parameters to access and store context information, which can sometimes proves error prone (especially for reconfigure jobs where we
* stuff in it two separate sub parameters
* - when some state needs to be skipped the logic is lousy
*/
class JainSipFsm {
FsmStates[] states;
JainSipJob.Type type;
JainSipClient jainSipClient;
static final String TAG = "JainSipFsm";
int index;
JainSipFsm(JainSipJob.Type type, JainSipClient jainSipClient)
{
init(type, jainSipClient);
}
void init(JainSipJob.Type type, JainSipClient jainSipClient)
{
this.type = type;
this.jainSipClient = jainSipClient;
index = 0;
if (type == JainSipJob.Type.TYPE_OPEN) {
states = new FsmStates[] { FsmStates.START_BIND_REGISTER, FsmStates.AUTH, FsmStates.NOTIFY};
}
else if (type == Type.TYPE_REGISTER_REFRESH) {
states = new FsmStates[] { FsmStates.REGISTER, FsmStates.AUTH, FsmStates.NOTIFY};
}
else if (type == Type.TYPE_CLOSE) {
states = new FsmStates[] { FsmStates.UNREGISTER, FsmStates.AUTH, FsmStates.SHUTDOWN};
}
else if (type == Type.TYPE_RECONFIGURE) {
states = new FsmStates[] { FsmStates.UNREGISTER, FsmStates.AUTH_1, FsmStates.REGISTER, FsmStates.AUTH_2, FsmStates.NOTIFY};
}
else if (type == Type.TYPE_RECONFIGURE_RELOAD_NETWORKING) {
states = new FsmStates[] { FsmStates.UNREGISTER, FsmStates.AUTH_1, FsmStates.UNBIND_BIND_REGISTER, FsmStates.AUTH_2, FsmStates.NOTIFY};
}
else if (type == Type.TYPE_RELOAD_NETWORKING) {
states = new FsmStates[] { FsmStates.UNBIND_BIND_REGISTER, FsmStates.AUTH, FsmStates.NOTIFY};
}
else if (type == Type.TYPE_START_NETWORKING) {
states = new FsmStates[] { FsmStates.BIND_REGISTER, FsmStates.AUTH, FsmStates.NOTIFY};
}
}
/**
* This is either called to start the FSM or resume it
* @param jobId Id for the current job
* @param event The input event provided by the caller, to help FSM understand what kind of transition to do
* @param arg Optional argument for the job
* @param statusCode Optional status code we want to convey (to the UI thread typically)
* @param statusText Optional the status text we want to convey (to the UI thread typically)
*/
void process(String jobId, FsmEvents event, Object arg, RCClient.ErrorCodes statusCode, String statusText)
{
if (statusCode == null) {
statusCode = RCClient.ErrorCodes.SUCCESS;
}
if (statusText == null) {
statusText = RCClient.errorText(RCClient.ErrorCodes.SUCCESS);
}
if (states == null) {
// if states is null, then it means that no FSM should be used at all
return;
}
if (JainSipJob.this.jobId.equals(jobId)) {
boolean loop;
do {
loop = false;
if (index >= states.length) {
RCLogger.e(TAG, "process(): no more states to process");
}
if (type == Type.TYPE_OPEN) {
// no matter what state we are in if we get a timeout we need to just bail
if (event.equals(FsmEvents.TIMEOUT)) {
jainSipClient.listener.onClientOpenedReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone,
RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_TIMEOUT,
RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_TIMEOUT));
jainSipJobManager.remove(jobId);
return;
}
if (states[index].equals(FsmStates.START_BIND_REGISTER)) {
try {
jainSipClient.jainSipClientStartStack();
if (!jainSipClient.jainSipNotificationManager.haveConnectivity()) {
jainSipClient.listener.onClientOpenedReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY,
RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY));
jainSipJobManager.remove(jobId);
return;
}
jainSipClient.jainSipClientBind(parameters);
if (parameters.containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN) && !parameters.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN).equals("")) {
// Domain has been provided do the registration
transaction = jainSipClient.jainSipClientRegister(JainSipJob.this, parameters);
}
else {
// No Domain there we are done here
jainSipClient.listener.onClientOpenedReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
RCClient.ErrorCodes.SUCCESS, RCClient.errorText(RCClient.ErrorCodes.SUCCESS));
jainSipJobManager.remove(jobId);
}
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientOpenedReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else if (states[index].equals(FsmStates.AUTH)) {
// the auth step is optional hence we check if auth-required event was passed by the caller, if not we loop around to visit next state
if (event.equals(FsmEvents.AUTH_REQUIRED)) {
ResponseEventExt responseEventExt = (ResponseEventExt) arg;
try {
jainSipClient.jainSipAuthenticate(JainSipJob.this, parameters, responseEventExt);
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientOpenedReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else {
loop = true;
}
}
else if (states[index].equals(FsmStates.NOTIFY)) {
if (event.equals(FsmEvents.REGISTER_FAILURE)) {
jainSipClient.listener.onClientOpenedReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, statusCode, statusText);
}
if (event.equals(FsmEvents.REGISTER_SUCCESS)) {
jainSipClient.listener.onClientOpenedReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
statusCode, statusText);
}
if (event.equals(FsmEvents.REGISTER_SUCCESS) || event.equals(FsmEvents.REGISTER_FAILURE)) {
jainSipJobManager.remove(jobId);
}
}
}
else if (type == Type.TYPE_REGISTER_REFRESH) {
// no matter what state we are in if we get a timeout we need to just bail
if (event.equals(FsmEvents.TIMEOUT)) {
jainSipClient.listener.onClientErrorReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone,
RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_TIMEOUT,
RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_TIMEOUT));
jainSipJobManager.remove(jobId);
return;
}
if (states[index].equals(FsmStates.REGISTER)) {
try {
transaction = jainSipClient.jainSipClientRegister(JainSipJob.this, parameters);
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientErrorReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else if (states[index].equals(FsmStates.AUTH)) {
// the auth step is optional hence we check if auth-required event was passed by the caller, if not we loop around to visit next state
if (event.equals(FsmEvents.AUTH_REQUIRED)) {
ResponseEventExt responseEventExt = (ResponseEventExt) arg;
try {
jainSipClient.jainSipAuthenticate(JainSipJob.this, parameters, responseEventExt);
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientErrorReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else {
loop = true;
}
}
else if (states[index].equals(FsmStates.NOTIFY)) {
if (event.equals(FsmEvents.REGISTER_FAILURE)) {
jainSipClient.listener.onClientErrorReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, statusCode, statusText);
}
if (event.equals(FsmEvents.REGISTER_SUCCESS) || event.equals(FsmEvents.REGISTER_FAILURE)) {
jainSipJobManager.remove(jobId);
}
}
}
else if (type == Type.TYPE_CLOSE) {
if (states[index].equals(FsmStates.UNREGISTER)) {
try {
transaction = jainSipClient.jainSipClientUnregister(parameters);
final String finalId = jobId;
// Schedule a check to see if we managed to close the signaling facilities. If not then we need to force closing.
// The reason we need that is for example if the unregister times out, which in SIP takes 32 seconds. This means
// that after the App is left the SIP stack will remain alive for 32 secs, which means that if user tries to re-open
// the stack it will fail.
Runnable runnable = new Runnable() {
@Override
public void run()
{
if (jainSipClient.jainSipJobManager.get(finalId) != null) {
RCLogger.e(TAG, "process(): Unregister is taking too long. Forcing signaling facilities to stop");
// failed to unregister; we need to unbind & stop stack or at next initialization we will fail
try {
// don't forget to terminate the transaction, or else the timeout will fire and will be useless
transaction.terminate();
jainSipClient.jainSipClientUnbind();
jainSipClient.jainSipClientStopStack();
}
catch (Exception e) {
// at this point we can't recover
throw new RuntimeException("Failed to release signaling facilities", e);
}
}
}
};
jainSipClient.signalingHandler.postDelayed(runnable, JainSipClient.FORCE_CLOSE_INTERVAL);
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientClosedEvent(jobId, e.errorCode, e.errorText);
// failed to unregister; we need to unbind & stop stack or at next initialization we will fail
try {
jainSipClient.jainSipClientUnbind();
jainSipClient.jainSipClientStopStack();
}
catch (JainSipException inner) {
// at this point we can't recover
throw new RuntimeException("Failed to unbind signaling facilities", e);
}
jainSipJobManager.remove(jobId);
}
}
else if (states[index].equals(FsmStates.AUTH)) {
// the auth step is optional hence we check if auth-required event was passed by the caller, if not we loop around to visit next state
if (event.equals(FsmEvents.AUTH_REQUIRED)) {
ResponseEventExt responseEventExt = (ResponseEventExt) arg;
try {
jainSipClient.jainSipAuthenticate(JainSipJob.this, parameters, responseEventExt);
}
catch (JainSipException e) {
// failed to unregister; we need to unbind & stop stack or at next initialization we will fail
try {
jainSipClient.jainSipClientUnbind();
jainSipClient.jainSipClientStopStack();
}
catch (JainSipException inner) {
// at this point we can't recover
throw new RuntimeException("Failed to unbind signaling facilities", e);
}
jainSipClient.listener.onClientClosedEvent(jobId, e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else {
loop = true;
}
}
else if (states[index].equals(FsmStates.SHUTDOWN)) {
if (event.equals(FsmEvents.REGISTER_SUCCESS) || event.equals(FsmEvents.REGISTER_FAILURE)) {
try {
jainSipClient.jainSipClientUnbind();
jainSipClient.jainSipClientStopStack();
jainSipClient.listener.onClientClosedEvent(jobId, RCClient.ErrorCodes.SUCCESS, RCClient.errorText(RCClient.ErrorCodes.SUCCESS));
}
catch (JainSipException e) {
// at this point we can't recover
throw new RuntimeException("Failed to unbind signaling facilities", e);
//jainSipClient.listener.onClientClosedEvent(jobId, e.errorCode, e.errorText);
}
jainSipJobManager.remove(jobId);
}
}
}
else if (type == Type.TYPE_RECONFIGURE) {
if (event.equals(FsmEvents.TIMEOUT)) {
// Important: if time out occurred on unregister we need to ignore and jump to register step. Take for example a case
// where the user does such a setup that registration fails and then they change again to a valid settings. In this case
// the first registration will timeout, but we don't care, we still need to continue with the register step
if (states[index].equals(FsmStates.AUTH_1)) {
// timeout occurred in unregister
RCLogger.w(TAG, "process(): unregister timed out in reconfigure, ignoring unregister step");
index += 1;
event = FsmEvents.REGISTER_FAILURE;
}
else {
jainSipClient.listener.onClientReconfigureReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone,
RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_TIMEOUT,
RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_TIMEOUT));
jainSipJobManager.remove(jobId);
return;
}
}
if (states[index].equals(FsmStates.UNREGISTER)) {
if (!jainSipClient.jainSipNotificationManager.haveConnectivity()) {
jainSipClient.listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY,
RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY));
jainSipJobManager.remove(jobId);
return;
}
try {
if (((HashMap<String, Object>) parameters.get("old-parameters")).containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN) &&
!((HashMap<String, Object>) parameters.get("old-parameters")).get(RCDevice.ParameterKeys.SIGNALING_DOMAIN).equals("")) {
// Domain has been provided do the registration
transaction = jainSipClient.jainSipClientUnregister((HashMap<String, Object>) parameters.get("old-parameters"));
}
else {
// No Domain, need to loop through to next step
loop = true;
// TODO: need to improve
// we need this to properly handle the 'register' step below
event = FsmEvents.REGISTER_SUCCESS;
}
}
catch (JainSipException e) {
e.printStackTrace();
// we failed to unregister, but this is a valid use case (like when user provides wrong domain). We need
// to avoid authentication and jump to registration with new settings
// TODO: this is a pretty messy way to convey that we want to jump 1 step
index += 1;
loop = true;
event = FsmEvents.REGISTER_FAILURE;
}
}
else if (states[index].equals(FsmStates.AUTH_1)) {
// the auth step is optional hence we check if auth-required event was passed by the caller, if not we loop around to visit next state
if (event.equals(FsmEvents.AUTH_REQUIRED)) {
ResponseEventExt responseEventExt = (ResponseEventExt) arg;
try {
jainSipClient.jainSipAuthenticate(JainSipJob.this, (HashMap<String, Object>) parameters.get("old-parameters"), responseEventExt);
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else {
loop = true;
}
}
else if (states[index].equals(FsmStates.REGISTER)) {
if (event.equals(FsmEvents.REGISTER_FAILURE)) {
// unregister step of reconfigure failed. Not that catastrophic, let's log it and continue; no need to notify UI thread just yet
RCLogger.e(TAG, "process(): unregister failed: " + Arrays.toString(Thread.currentThread().getStackTrace()));
}
if (event.equals(FsmEvents.REGISTER_SUCCESS) || event.equals(FsmEvents.REGISTER_FAILURE)) {
try {
if (((HashMap<String, Object>) parameters.get("new-parameters")).containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN) &&
!((HashMap<String, Object>) parameters.get("new-parameters")).get(RCDevice.ParameterKeys.SIGNALING_DOMAIN).equals("")) {
// Domain has been provided do the registration
transaction = jainSipClient.jainSipClientRegister(JainSipJob.this, (HashMap<String, Object>) parameters.get("new-parameters"));
}
else {
// No domain, need to loop through to next step
loop = true;
}
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
}
else if (states[index].equals(FsmStates.AUTH_2)) {
// the auth step is optional hence we check if auth-required event was passed by the caller, if not we loop around to visit next state
if (event.equals(FsmEvents.AUTH_REQUIRED)) {
ResponseEventExt responseEventExt = (ResponseEventExt) arg;
try {
jainSipClient.jainSipAuthenticate(JainSipJob.this, (HashMap<String, Object>) parameters.get("new-parameters"), responseEventExt);
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else {
loop = true;
}
}
else if (states[index].equals(FsmStates.NOTIFY)) {
if (event.equals(FsmEvents.REGISTER_SUCCESS) || event.equals(FsmEvents.REGISTER_FAILURE)) {
jainSipClient.listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
statusCode, statusText);
jainSipJobManager.remove(jobId);
}
}
}
else if (type == Type.TYPE_RECONFIGURE_RELOAD_NETWORKING) {
if (states[index].equals(FsmStates.UNREGISTER)) {
if (!jainSipClient.jainSipNotificationManager.haveConnectivity()) {
jainSipClient.listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY,
RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_NO_CONNECTIVITY));
jainSipJobManager.remove(jobId);
return;
}
try {
if (((HashMap<String, Object>) parameters.get("old-parameters")).containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN) &&
!((HashMap<String, Object>) parameters.get("old-parameters")).get(RCDevice.ParameterKeys.SIGNALING_DOMAIN).equals("")) {
// Domain has been provided do the registration
transaction = jainSipClient.jainSipClientUnregister((HashMap<String, Object>) parameters.get("old-parameters"));
}
else {
// No domain, need to loop through to next step
// TODO: this is a pretty messy way to convey that we want to jump 1 step
index += 1;
loop = true;
event = FsmEvents.REGISTER_SUCCESS;
}
}
catch (JainSipException e) {
e.printStackTrace();
// we failed to unregister, but this is a valid use case (like when user provides wrong domain). We need
// to avoid authentication and jump to registration with new settings
// TODO: this is a pretty messy way to convey that we want to jump 1 step
index += 1;
loop = true;
event = FsmEvents.REGISTER_FAILURE;
}
}
else if (states[index].equals(FsmStates.AUTH_1)) {
// the auth step is optional hence we check if auth-required event was passed by the caller, if not we loop around to visit next state
if (event.equals(FsmEvents.AUTH_REQUIRED)) {
ResponseEventExt responseEventExt = (ResponseEventExt) arg;
try {
jainSipClient.jainSipAuthenticate(JainSipJob.this, (HashMap<String, Object>) parameters.get("old-parameters"), responseEventExt);
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else {
loop = true;
}
}
else if (states[index].equals(FsmStates.UNBIND_BIND_REGISTER)) {
if (event.equals(FsmEvents.REGISTER_FAILURE)) {
// unregister step of reconfigure failed. Not that catastrophic, let's log it and continue; no need to notify UI thread just yet
RCLogger.e(TAG, "process(): unregister failed: " + Arrays.toString(Thread.currentThread().getStackTrace()));
}
if (event.equals(FsmEvents.REGISTER_SUCCESS) || event.equals(FsmEvents.REGISTER_FAILURE)) {
try {
jainSipClient.jainSipClientUnbind();
jainSipClient.jainSipClientBind((HashMap<String, Object>) parameters.get("new-parameters"));
if (((HashMap<String, Object>) parameters.get("new-parameters")).containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN) &&
!((HashMap<String, Object>) parameters.get("new-parameters")).get(RCDevice.ParameterKeys.SIGNALING_DOMAIN).equals("")) {
// Domain has been provided do the registration
transaction = jainSipClient.jainSipClientRegister(JainSipJob.this, (HashMap<String, Object>) parameters.get("new-parameters"));
}
else {
// No domain, need to loop through to next step
// TODO: this is a pretty messy way to convey that we want to jump 1 step
index += 1;
loop = true;
event = FsmEvents.REGISTER_FAILURE;
}
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
}
else if (states[index].equals(FsmStates.AUTH_2)) {
// the auth step is optional hence we check if auth-required event was passed by the caller, if not we loop around to visit next state
if (event.equals(FsmEvents.AUTH_REQUIRED)) {
ResponseEventExt responseEventExt = (ResponseEventExt) arg;
try {
jainSipClient.jainSipAuthenticate(JainSipJob.this, (HashMap<String, Object>) parameters.get("new-parameters"), responseEventExt);
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else {
loop = true;
}
}
else if (states[index].equals(FsmStates.NOTIFY)) {
if (event.equals(FsmEvents.REGISTER_SUCCESS) || event.equals(FsmEvents.REGISTER_FAILURE)) {
jainSipClient.listener.onClientReconfigureReply(jobId, JainSipNotificationManager.networkStatus2ConnectivityStatus(jainSipClient.jainSipNotificationManager.getNetworkStatus()),
statusCode, statusText);
jainSipJobManager.remove(jobId);
}
}
}
else if (type == Type.TYPE_RELOAD_NETWORKING) {
if (states[index].equals(FsmStates.UNBIND_BIND_REGISTER)) {
// no need for connectivity check here, we know there is connectivity
try {
jainSipClient.jainSipClientUnbind();
jainSipClient.jainSipClientBind(parameters);
if (parameters.containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN) && !parameters.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN).equals("")) {
// Domain has been provided do the registration
transaction = jainSipClient.jainSipClientRegister(JainSipJob.this, parameters);
}
else {
// No domain, need to loop through to next step
RCDeviceListener.RCConnectivityStatus connectivityStatus = (RCDeviceListener.RCConnectivityStatus) parameters.get("connectivity-status");
jainSipClient.listener.onClientConnectivityEvent(jobId, connectivityStatus);
jainSipJobManager.remove(jobId);
}
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientErrorReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else if (states[index].equals(FsmStates.AUTH)) {
// the auth step is optional hence we check if auth-required event was passed by the caller, if not we loop around to visit next state
if (event.equals(FsmEvents.AUTH_REQUIRED)) {
ResponseEventExt responseEventExt = (ResponseEventExt) arg;
try {
jainSipClient.jainSipAuthenticate(JainSipJob.this, parameters, responseEventExt);
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientErrorReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else {
loop = true;
}
}
else if (states[index].equals(FsmStates.NOTIFY)) {
if (event.equals(FsmEvents.REGISTER_SUCCESS)) {
RCDeviceListener.RCConnectivityStatus connectivityStatus = (RCDeviceListener.RCConnectivityStatus) parameters.get("connectivity-status");
jainSipClient.listener.onClientConnectivityEvent(jobId, connectivityStatus);
jainSipJobManager.remove(jobId);
}
if (event.equals(FsmEvents.REGISTER_FAILURE)) {
jainSipClient.listener.onClientErrorReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, statusCode, statusText);
jainSipJobManager.remove(jobId);
}
}
}
else if (type == Type.TYPE_START_NETWORKING) {
// no matter what state we are in if we get a timeout we need to remove job and forget about it
if (event.equals(FsmEvents.TIMEOUT)) {
jainSipClient.listener.onClientOpenedReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone,
RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_TIMEOUT,
RCClient.errorText(RCClient.ErrorCodes.ERROR_DEVICE_REGISTER_TIMEOUT));
jainSipJobManager.remove(jobId);
return;
}
RCLogger.i(TAG, "Job, TYPE_START_NETWORKING: " + this.toString());
if (states[index].equals(FsmStates.BIND_REGISTER)) {
// no need for connectivity check here, we know there is connectivity
try {
jainSipClient.jainSipClientBind(parameters);
if (parameters.containsKey(RCDevice.ParameterKeys.SIGNALING_DOMAIN) && !parameters.get(RCDevice.ParameterKeys.SIGNALING_DOMAIN).equals("")) {
// Domain has been provided do the registration
transaction = jainSipClient.jainSipClientRegister(JainSipJob.this, parameters);
}
else {
// No Domain there we are done here
RCDeviceListener.RCConnectivityStatus connectivityStatus = (RCDeviceListener.RCConnectivityStatus) parameters.get("connectivity-status");
jainSipClient.listener.onClientConnectivityEvent(jobId, connectivityStatus);
jainSipJobManager.remove(jobId);
}
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientErrorReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else if (states[index].equals(FsmStates.AUTH)) {
// the auth step is optional hence we check if auth-required event was passed by the caller, if not we loop around to visit next state
if (event.equals(FsmEvents.AUTH_REQUIRED)) {
ResponseEventExt responseEventExt = (ResponseEventExt) arg;
try {
jainSipClient.jainSipAuthenticate(JainSipJob.this, parameters, responseEventExt);
}
catch (JainSipException e) {
e.printStackTrace();
jainSipClient.listener.onClientErrorReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, e.errorCode, e.errorText);
jainSipJobManager.remove(jobId);
}
}
else {
loop = true;
}
}
else if (states[index].equals(FsmStates.NOTIFY)) {
if (event.equals(FsmEvents.REGISTER_SUCCESS)) {
RCDeviceListener.RCConnectivityStatus connectivityStatus = (RCDeviceListener.RCConnectivityStatus) parameters.get("connectivity-status");
jainSipClient.listener.onClientConnectivityEvent(jobId, connectivityStatus);
jainSipJobManager.remove(jobId);
}
if (event.equals(FsmEvents.REGISTER_FAILURE)) {
jainSipClient.listener.onClientErrorReply(jobId, RCDeviceListener.RCConnectivityStatus.RCConnectivityStatusNone, statusCode, statusText);
jainSipJobManager.remove(jobId);
}
}
}
index++;
} while (loop);
}
}
@Override
public String toString()
{
StringBuilder result = new StringBuilder();
//String NEW_LINE = System.getProperty("line.separator");
result.append(this.getClass().getName() + " Object { ");
result.append("Type: " + type + ", ");
result.append("Index: " + index + " ");
result.append("}");
return result.toString();
}
}
public enum Type {
TYPE_OPEN,
TYPE_REGISTER_REFRESH,
TYPE_CLOSE,
TYPE_RECONFIGURE,
TYPE_RECONFIGURE_RELOAD_NETWORKING,
TYPE_RELOAD_NETWORKING,
TYPE_START_NETWORKING,
TYPE_CALL,
TYPE_MESSAGE,
}
// jobId is a unique identifier for a Job. It is App provided for outgoing requests (typically Unix time with miliseconds, as a string)
// and SIP Call-Id for incoming requests. Notice that for outgoing requests the App provided jobId is also used as SIP Call-ID, to make
// troubleshooting easier
public String jobId;
public Type type;
// current JAIN sip transaction this job is currently executing. Remember that usually one Job is made up of multiple transactions occuring one after another
public Transaction transaction;
public HashMap<String, Object> parameters;
public JainSipCall jainSipCall;
public int authenticationAttempts;
public static int MAX_AUTH_ATTEMPTS = 3;
JainSipClient jainSipClient;
JainSipJobManager jainSipJobManager;
JainSipFsm jainSipFsm;
static final String TAG = "JainSipJob";
JainSipJob(JainSipJobManager jainSipJobManager, JainSipClient jainSipClient, String jobId, Type type, Transaction transaction, HashMap<String, Object> parameters,
JainSipCall jainSipCall)
{
this.jobId = jobId;
this.type = type;
this.transaction = transaction;
this.parameters = parameters;
this.authenticationAttempts = 0;
this.jainSipClient = jainSipClient;
this.jainSipJobManager = jainSipJobManager;
this.jainSipFsm = new JainSipFsm(type, jainSipClient);
this.jainSipCall = jainSipCall;
}
void startFsm()
{
jainSipFsm.process(jobId, FsmEvents.NONE, null, null, null);
}
// Not all jobs have FSM. Simple jobs don't need one. Check if current job has an FSM
boolean hasFsm()
{
if (type == JainSipJob.Type.TYPE_OPEN ||
type == Type.TYPE_REGISTER_REFRESH ||
type == Type.TYPE_CLOSE ||
type == Type.TYPE_RECONFIGURE ||
type == Type.TYPE_RECONFIGURE_RELOAD_NETWORKING ||
type == Type.TYPE_RELOAD_NETWORKING ||
type == Type.TYPE_START_NETWORKING) {
return true;
}
return false;
}
void processFsm(String jobId, FsmEvents event, Object arg, RCClient.ErrorCodes statusCode, String statusText)
{
if (hasFsm()) {
jainSipFsm.process(jobId, event, arg, statusCode, statusText);
}
}
void updateTransaction(Transaction transaction)
{
this.transaction = transaction;
}
// Should we retry authentication if previous failed? We retry a max of MAX_AUTH_ATTEMPTS
boolean shouldRetry()
{
if (authenticationAttempts < JainSipJob.MAX_AUTH_ATTEMPTS - 1) {
return true;
}
else {
return false;
}
}
void increaseAuthAttempts()
{
authenticationAttempts += 1;
}
@Override
public String toString()
{
StringBuilder result = new StringBuilder();
String NEW_LINE = System.getProperty("line.separator");
result.append(this.getClass().getName() + " Object {" + NEW_LINE);
result.append(" Job Id: " + jobId + NEW_LINE);
result.append(" Type: " + type + NEW_LINE);
result.append(" Transaction: " + transaction + NEW_LINE);
result.append(" Parameters: " + parameters + NEW_LINE);
result.append(" Fsm: " + jainSipFsm + NEW_LINE);
result.append("}");
return result.toString();
}
}