public class SipClient extends java.lang.Object implements android.javax.sip.SipListener, ConnectivityReceiver.IPChangeListener
This is the core of the SIP layer, and the entire app. All SIP messages are received here, and sent from here via SipRequester/SipResponder/SipTransactionRequester. It is a singleton, as it makes up much of the app, so the proper usage is first call init() and then call getInstance() whenever a handle to this class is needed. If necessary later, calling reset() re-initializes some of the parameters from init(). When disconnecting from the server or quitting, it is necessary to call close().
-------------- | UI layer | -------------- | -------------- | SIP layer | (SipClient) -------------- | -------------- | Call layer | (RTTCall) -------------- | -------------- | RTP layer | (JRTP, Omnitor t140) --------------
Though RTTCall runs the actual RTP sessions, SipClient is responsible for negotiating the session, and it can only manage one call at a time. It knows when a call is currently connected, or ringing, because it arranges it via SIP messages, interaction from the UI layer above, and creating each RTTCall. If a call comes in while another one is in progress, SipClient responds 486 Busy Here. onACallNow() indicates whether a call is in progress. An RTTCall is created early in the call establishing sequence, and SipClient later updates RTTCall when the call is fully agreed upon and the RTP parameters are available.
The incoming call (acceptance) sequence is a bit hairy and involves the other layers. This could probably use some refactoring or at least refinement to ensure reliability. This sequence grew out of experience with the concurrency problems surrounding duplicate or multiple requests coming in simultaneously, and the desire to only handle one call at a time. The steps are as follows:
1. INVITE received 2. receiveCall(): a. tries to acquire callLock, and holds it to prevent any other calls being created b. checks if onACallNow() c. responds 180 Ringing d. creates RTTCall e. alerts callReceiver (CallReceiver listener in UI layer) that a call is ringing 3. UI layer accepts or declines call a. calls SipClient.acceptCall() b. or SipClient.declineCall() 4. acceptCall(): a. sends 200 OK response b. releases callLock, and now relies on onACallNow() to detect business 5. ACK received 6. beginCall(): a. gives RTP params to RTTCall b. waits for UI to get ready to receive text c. updates RTTCall's list of TextReceivers and tells it to start d. tells UI layer that call is established with notifySessionEstablished()
Some of this complication is unavoidable due to SIP's multi-step process, but the locking is especially weird, I know. It's sketchy to acquire a lock, wait for some other part of the app to do something, assume the lock is still held, and then later release it. The purpose of the lock is to prevent simultaneous INVITEs from triggering receiveCall(), so it seems like it could be released after creating the RTTCall, but my fuzzy memory suggests that some problem occurred. There's a reason I didn't release the lock until later, but now that I have forgotten it, I feel bad telling you not to change this. This might be ok to change, but beware. Getting it right with all the incoming traffic I was seeing was tough, so I'm wary of breaking it, but it's probably fragile as it is now anyway.
All network communication in this class, and indeed this layer, is done by helper AsyncTask subclasses, e.g. SipRequester. This might not really be necessary. Android is strict about using the network on the main thread, which is usually good, but in this case sort of annoying. You don't really want to be doing TCP on the main thread, but the only network stuff that SipClient is doing is sending SIP messages over UDP, which basically happens immediately, no waiting involved. So we could disable this restriction with StrictMode.ThreadPolicy.LAX? This would allow us to get rid of the unwieldy AsyncTasks. The main concern is whether setting this global option would have other unintended consequences in other parts of the app.
SipClient is listening for changes in internet connectivity, specifically changes in the IP address, which happens often enough to really care about it on a mobile device. When the IP changes, it must re-register with the server at the new address. If this happens during a call, the call will mysteriously hang, so SipClient will end a call if it detects this change.
Other classes often need to receive messages from SipClient, when network/call state changes. Thus it has lists of listeners of various types, managed with methods such as addSessionListener()/removeSessionListener(). When an event occurs that interests those listeners, SipClient notifies all of them, if there is more than one, with a method such as e.g. notifySessionClosed().
Currently SipClient does not do a great job correlating sequences of messages, e.g. a request we sent and the response to it that we get back. It should probably maintain a data structure of Requests and Responses it has sent, and the transactions/dialogs involved in them, to definitively know the purpose of some incoming message, whether a SIP message or something from the SipStack, like a TransactionTerminated message. This would also help weed out stray messages that we don't care about.
Some of this class based on http://alex.bikfalvi.com/teaching/upf/2013/architecture_and_signaling/lab/sip/
Modifier and Type | Method and Description |
---|---|
void |
acceptCall()
The CallReceiver (likely on an upper layer) must call either this or declineCall()
when an incoming call is ringing.
|
void |
addSessionListener(SessionListener newListener)
register a listener as interested in receiving session-related notifications
|
void |
addTextReceiver(TextListener newReceiver)
This method should be called when a user of SipClient needs to register a new
receiver for the SipClient's text output.
|
void |
call(java.lang.String URI)
Initiate a new call to the given SIP URI.
|
void |
close()
This must be called when the SipClient is done and will not be used again, to close ports
and delete broadcast receivers.
|
void |
declineCall()
The CallReceiver (likely on an upper layer) must call either this or acceptCall()
when an incoming call is ringing.
|
void |
finalize()
In case close() is not called properly by the rest of the app
|
static SipClient |
getInstance()
Get a handle to the single global SipClient
Precondition: init() must be called first at least once
|
java.lang.String |
getLocalIP() |
void |
hangUp()
This is a public method for an upper layer class to notify the SipClient that it wants to end
the call.
|
static void |
init(android.content.Context context,
java.lang.String username,
java.lang.String server,
java.lang.String password,
TextListener listener)
Initializes the SipClient to prepare it to register with a server and make calls.
|
void |
IPAddrChanged()
ConnectivityReceiver.IPChangeListener interface callback
|
void |
processDialogTerminated(android.javax.sip.DialogTerminatedEvent dialogTerminatedEvent)
SipListener interface callback
|
void |
processIOException(android.javax.sip.IOExceptionEvent ioExceptionEvent)
SipListener interface callback
|
void |
processRequest(android.javax.sip.RequestEvent requestEvent)
SipListener interface callback.
|
void |
processResponse(android.javax.sip.ResponseEvent responseEvent)
SipListener interface callback.
|
void |
processTimeout(android.javax.sip.TimeoutEvent timeoutEvent)
SipListener interface callback
|
void |
processTransactionTerminated(android.javax.sip.TransactionTerminatedEvent transactionTerminatedEvent)
SipListener interface callback
|
void |
register()
tell the SipClient to register with the SIP server, given the credentials it currently has
on hand.
|
void |
registerCallReceiver(CallReceiver receiver)
Register some object as interested in listening for incoming calls.
|
void |
registrationExpired()
RegistrationTimerTask interface callback
|
void |
removeSessionListener(SessionListener existingListener)
remove a listener from receiving session notifications
|
void |
removeTextReceiver(TextListener receiver) |
void |
reset(android.content.Context context,
java.lang.String username,
java.lang.String server,
java.lang.String password,
TextListener listener) |
void |
sendRTTChars(java.lang.String add)
Send some real-time text within the current call.
|
void |
unregister()
Tell the SipClient to remove its registration with the server, e.g.
|
public static void init(android.content.Context context, java.lang.String username, java.lang.String server, java.lang.String password, TextListener listener) throws android.javax.sip.SipException
context
- username
- server
- password
- listener
- android.javax.sip.SipException
- if the SIP stack can't be created, or the username/password/server can't be parsed correctlypublic void reset(android.content.Context context, java.lang.String username, java.lang.String server, java.lang.String password, TextListener listener) throws android.javax.sip.SipException
android.javax.sip.SipException
public static SipClient getInstance() throws java.lang.IllegalStateException
java.lang.IllegalStateException
public java.lang.String getLocalIP()
public void addTextReceiver(TextListener newReceiver)
newReceiver
- the new object (e.g. an Activity) that wants to handle text
messages from from the SipClient. If null, the SipClient's
text output will not be processed by anyone.public void registerCallReceiver(CallReceiver receiver)
receiver
- the CallReceiver who should be notified when a call comes in.
There can only be one receiver listening for this message at a time.public void removeTextReceiver(TextListener receiver)
receiver
- the TextReceiver that should no longer receive text messagespublic void addSessionListener(SessionListener newListener)
newListener
- the new listener. If it already registered, nothing happenspublic void removeSessionListener(SessionListener existingListener)
existingListener
- the listener to removepublic void sendRTTChars(java.lang.String add) throws java.lang.IllegalStateException
add
- the characters to send in the RTT sessionjava.lang.IllegalStateException
- if not connected on a callpublic void register() throws android.javax.sip.SipException
android.javax.sip.SipException
public void unregister() throws android.javax.sip.SipException
android.javax.sip.SipException
public void finalize()
finalize
in class java.lang.Object
public void close()
public void call(java.lang.String URI) throws android.javax.sip.SipException, java.text.ParseException, android.javax.sip.TransactionUnavailableException, java.lang.InterruptedException, java.util.concurrent.ExecutionException, android.javax.sip.InvalidArgumentException
URI
- the other party's SIP addressandroid.javax.sip.SipException
- if the request couldn't be sent for some other reasonjava.text.ParseException
- if the URI can't be parsed, or the contact's server is not validandroid.javax.sip.TransactionUnavailableException
- if already on a call, or if another error occursjava.lang.InterruptedException
java.util.concurrent.ExecutionException
android.javax.sip.InvalidArgumentException
public void processRequest(android.javax.sip.RequestEvent requestEvent)
processRequest
in interface android.javax.sip.SipListener
public void hangUp()
public void acceptCall() throws java.lang.IllegalStateException
java.lang.IllegalStateException
- if there is no call waiting to be accepted (possibly because it already hung up),
or if we are otherwise not in the midst of setting up a new callpublic void declineCall() throws java.lang.IllegalStateException
java.lang.IllegalStateException
- if there is no call waiting to be accepted (possibly because it already hung up),
or if we are otherwise not in the midst of setting up a new callpublic void processResponse(android.javax.sip.ResponseEvent responseEvent)
processResponse
in interface android.javax.sip.SipListener
public void registrationExpired()
public void processTimeout(android.javax.sip.TimeoutEvent timeoutEvent)
processTimeout
in interface android.javax.sip.SipListener
public void processIOException(android.javax.sip.IOExceptionEvent ioExceptionEvent)
processIOException
in interface android.javax.sip.SipListener
public void processTransactionTerminated(android.javax.sip.TransactionTerminatedEvent transactionTerminatedEvent)
processTransactionTerminated
in interface android.javax.sip.SipListener
public void processDialogTerminated(android.javax.sip.DialogTerminatedEvent dialogTerminatedEvent)
processDialogTerminated
in interface android.javax.sip.SipListener
public void IPAddrChanged()
IPAddrChanged
in interface ConnectivityReceiver.IPChangeListener