| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.sip; |
| |
| import gov.nist.javax.sip.clientauthutils.AccountManager; |
| import gov.nist.javax.sip.clientauthutils.UserCredentials; |
| import gov.nist.javax.sip.header.SIPHeaderNames; |
| import gov.nist.javax.sip.header.WWWAuthenticate; |
| import gov.nist.javax.sip.message.SIPMessage; |
| |
| import android.net.sip.ISipSession; |
| import android.net.sip.ISipSessionListener; |
| import android.net.sip.SessionDescription; |
| import android.net.sip.SipErrorCode; |
| import android.net.sip.SipProfile; |
| import android.net.sip.SipSessionAdapter; |
| import android.net.sip.SipSessionState; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import java.io.IOException; |
| import java.io.UnsupportedEncodingException; |
| import java.net.DatagramSocket; |
| import java.net.UnknownHostException; |
| import java.text.ParseException; |
| import java.util.Collection; |
| import java.util.EventObject; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.TooManyListenersException; |
| |
| import javax.sip.ClientTransaction; |
| import javax.sip.Dialog; |
| import javax.sip.DialogTerminatedEvent; |
| import javax.sip.IOExceptionEvent; |
| import javax.sip.InvalidArgumentException; |
| import javax.sip.ListeningPoint; |
| import javax.sip.RequestEvent; |
| import javax.sip.ResponseEvent; |
| import javax.sip.ServerTransaction; |
| import javax.sip.SipException; |
| import javax.sip.SipFactory; |
| import javax.sip.SipListener; |
| import javax.sip.SipProvider; |
| import javax.sip.SipStack; |
| import javax.sip.TimeoutEvent; |
| import javax.sip.Transaction; |
| import javax.sip.TransactionState; |
| import javax.sip.TransactionTerminatedEvent; |
| import javax.sip.TransactionUnavailableException; |
| import javax.sip.address.Address; |
| import javax.sip.address.SipURI; |
| import javax.sip.header.CSeqHeader; |
| import javax.sip.header.ExpiresHeader; |
| import javax.sip.header.FromHeader; |
| import javax.sip.header.MinExpiresHeader; |
| import javax.sip.header.ViaHeader; |
| import javax.sip.message.Message; |
| import javax.sip.message.Request; |
| import javax.sip.message.Response; |
| |
| /** |
| * Manages {@link ISipSession}'s for a SIP account. |
| */ |
| class SipSessionGroup implements SipListener { |
| private static final String TAG = "SipSession"; |
| private static final String ANONYMOUS = "anonymous"; |
| private static final String SERVER_ERROR_PREFIX = "Response: "; |
| private static final int EXPIRY_TIME = 3600; |
| |
| private static final EventObject DEREGISTER = new EventObject("Deregister"); |
| private static final EventObject END_CALL = new EventObject("End call"); |
| private static final EventObject HOLD_CALL = new EventObject("Hold call"); |
| private static final EventObject CONTINUE_CALL |
| = new EventObject("Continue call"); |
| |
| private final SipProfile mLocalProfile; |
| private final String mPassword; |
| |
| private SipStack mSipStack; |
| private SipHelper mSipHelper; |
| private String mLastNonce; |
| private int mRPort; |
| |
| // session that processes INVITE requests |
| private SipSessionImpl mCallReceiverSession; |
| private String mLocalIp; |
| |
| // call-id-to-SipSession map |
| private Map<String, SipSessionImpl> mSessionMap = |
| new HashMap<String, SipSessionImpl>(); |
| |
| /** |
| * @param myself the local profile with password crossed out |
| * @param password the password of the profile |
| * @throws IOException if cannot assign requested address |
| */ |
| public SipSessionGroup(String localIp, SipProfile myself, String password) |
| throws SipException, IOException { |
| mLocalProfile = myself; |
| mPassword = password; |
| reset(localIp); |
| } |
| |
| void reset(String localIp) throws SipException, IOException { |
| mLocalIp = localIp; |
| if (localIp == null) return; |
| |
| SipProfile myself = mLocalProfile; |
| SipFactory sipFactory = SipFactory.getInstance(); |
| Properties properties = new Properties(); |
| properties.setProperty("javax.sip.STACK_NAME", getStackName()); |
| String outboundProxy = myself.getProxyAddress(); |
| if (!TextUtils.isEmpty(outboundProxy)) { |
| Log.v(TAG, "outboundProxy is " + outboundProxy); |
| properties.setProperty("javax.sip.OUTBOUND_PROXY", outboundProxy |
| + ":" + myself.getPort() + "/" + myself.getProtocol()); |
| } |
| SipStack stack = mSipStack = sipFactory.createSipStack(properties); |
| |
| try { |
| SipProvider provider = stack.createSipProvider( |
| stack.createListeningPoint(localIp, allocateLocalPort(), |
| myself.getProtocol())); |
| provider.addSipListener(this); |
| mSipHelper = new SipHelper(stack, provider); |
| } catch (InvalidArgumentException e) { |
| throw new IOException(e.getMessage()); |
| } catch (TooManyListenersException e) { |
| // must never happen |
| throw new SipException("SipSessionGroup constructor", e); |
| } |
| Log.d(TAG, " start stack for " + myself.getUriString()); |
| stack.start(); |
| |
| mLastNonce = null; |
| mCallReceiverSession = null; |
| mSessionMap.clear(); |
| } |
| |
| public SipProfile getLocalProfile() { |
| return mLocalProfile; |
| } |
| |
| public String getLocalProfileUri() { |
| return mLocalProfile.getUriString(); |
| } |
| |
| private String getStackName() { |
| return "stack" + System.currentTimeMillis(); |
| } |
| |
| public synchronized void close() { |
| Log.d(TAG, " close stack for " + mLocalProfile.getUriString()); |
| mSessionMap.clear(); |
| closeToNotReceiveCalls(); |
| if (mSipStack != null) { |
| mSipStack.stop(); |
| mSipStack = null; |
| mSipHelper = null; |
| } |
| } |
| |
| public synchronized boolean isClosed() { |
| return (mSipStack == null); |
| } |
| |
| // For internal use, require listener not to block in callbacks. |
| public synchronized void openToReceiveCalls(ISipSessionListener listener) { |
| if (mCallReceiverSession == null) { |
| mCallReceiverSession = new SipSessionCallReceiverImpl(listener); |
| } else { |
| mCallReceiverSession.setListener(listener); |
| } |
| } |
| |
| public synchronized void closeToNotReceiveCalls() { |
| mCallReceiverSession = null; |
| } |
| |
| public ISipSession createSession(ISipSessionListener listener) { |
| return (isClosed() ? null : new SipSessionImpl(listener)); |
| } |
| |
| private static int allocateLocalPort() throws SipException { |
| try { |
| DatagramSocket s = new DatagramSocket(); |
| int localPort = s.getLocalPort(); |
| s.close(); |
| return localPort; |
| } catch (IOException e) { |
| throw new SipException("allocateLocalPort()", e); |
| } |
| } |
| |
| private synchronized SipSessionImpl getSipSession(EventObject event) { |
| String key = SipHelper.getCallId(event); |
| Log.d(TAG, " sesssion key from event: " + key); |
| Log.d(TAG, " active sessions:"); |
| for (String k : mSessionMap.keySet()) { |
| Log.d(TAG, " ..... '" + k + "': " + mSessionMap.get(k)); |
| } |
| SipSessionImpl session = mSessionMap.get(key); |
| return ((session != null) ? session : mCallReceiverSession); |
| } |
| |
| private synchronized void addSipSession(SipSessionImpl newSession) { |
| removeSipSession(newSession); |
| String key = newSession.getCallId(); |
| Log.d(TAG, " +++++ add a session with key: '" + key + "'"); |
| mSessionMap.put(key, newSession); |
| for (String k : mSessionMap.keySet()) { |
| Log.d(TAG, " ..... " + k + ": " + mSessionMap.get(k)); |
| } |
| } |
| |
| private synchronized void removeSipSession(SipSessionImpl session) { |
| if (session == mCallReceiverSession) return; |
| String key = session.getCallId(); |
| SipSessionImpl s = mSessionMap.remove(key); |
| // sanity check |
| if ((s != null) && (s != session)) { |
| Log.w(TAG, "session " + session + " is not associated with key '" |
| + key + "'"); |
| mSessionMap.put(key, s); |
| for (Map.Entry<String, SipSessionImpl> entry |
| : mSessionMap.entrySet()) { |
| if (entry.getValue() == s) { |
| key = entry.getKey(); |
| mSessionMap.remove(key); |
| } |
| } |
| } |
| Log.d(TAG, " remove session " + session + " with key '" + key + "'"); |
| |
| for (String k : mSessionMap.keySet()) { |
| Log.d(TAG, " ..... " + k + ": " + mSessionMap.get(k)); |
| } |
| } |
| |
| public void processRequest(RequestEvent event) { |
| process(event); |
| } |
| |
| public void processResponse(ResponseEvent event) { |
| process(event); |
| } |
| |
| public void processIOException(IOExceptionEvent event) { |
| process(event); |
| } |
| |
| public void processTimeout(TimeoutEvent event) { |
| process(event); |
| } |
| |
| public void processTransactionTerminated(TransactionTerminatedEvent event) { |
| process(event); |
| } |
| |
| public void processDialogTerminated(DialogTerminatedEvent event) { |
| process(event); |
| } |
| |
| private synchronized void process(EventObject event) { |
| SipSessionImpl session = getSipSession(event); |
| try { |
| if ((session != null) && session.process(event)) { |
| Log.d(TAG, " ~~~~~ new state: " + session.mState); |
| } else { |
| Log.d(TAG, "event not processed: " + event); |
| } |
| } catch (Throwable e) { |
| Log.w(TAG, "event process error: " + event, e); |
| session.onError(e); |
| } |
| } |
| |
| private String extractContent(Message message) { |
| // Currently we do not support secure MIME bodies. |
| byte[] bytes = message.getRawContent(); |
| if (bytes != null) { |
| try { |
| if (message instanceof SIPMessage) { |
| return ((SIPMessage) message).getMessageContent(); |
| } else { |
| return new String(bytes, "UTF-8"); |
| } |
| } catch (UnsupportedEncodingException e) { |
| } |
| } |
| return null; |
| } |
| |
| private class SipSessionCallReceiverImpl extends SipSessionImpl { |
| public SipSessionCallReceiverImpl(ISipSessionListener listener) { |
| super(listener); |
| } |
| |
| public boolean process(EventObject evt) throws SipException { |
| Log.d(TAG, " ~~~~~ " + this + ": " + mState + ": processing " |
| + log(evt)); |
| if (isRequestEvent(Request.INVITE, evt)) { |
| RequestEvent event = (RequestEvent) evt; |
| SipSessionImpl newSession = new SipSessionImpl(mProxy); |
| newSession.mServerTransaction = mSipHelper.sendRinging(event, |
| generateTag()); |
| newSession.mDialog = newSession.mServerTransaction.getDialog(); |
| newSession.mInviteReceived = event; |
| newSession.mPeerProfile = createPeerProfile(event.getRequest()); |
| newSession.mState = SipSessionState.INCOMING_CALL; |
| newSession.mPeerSessionDescription = |
| extractContent(event.getRequest()); |
| addSipSession(newSession); |
| mProxy.onRinging(newSession, newSession.mPeerProfile, |
| newSession.mPeerSessionDescription); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| } |
| |
| class SipSessionImpl extends ISipSession.Stub { |
| SipProfile mPeerProfile; |
| SipSessionListenerProxy mProxy = new SipSessionListenerProxy(); |
| SipSessionState mState = SipSessionState.READY_TO_CALL; |
| RequestEvent mInviteReceived; |
| Dialog mDialog; |
| ServerTransaction mServerTransaction; |
| ClientTransaction mClientTransaction; |
| String mPeerSessionDescription; |
| boolean mInCall; |
| boolean mReRegisterFlag = false; |
| |
| public SipSessionImpl(ISipSessionListener listener) { |
| setListener(listener); |
| } |
| |
| SipSessionImpl duplicate() { |
| return new SipSessionImpl(mProxy.getListener()); |
| } |
| |
| private void reset() { |
| mInCall = false; |
| removeSipSession(this); |
| mPeerProfile = null; |
| mState = SipSessionState.READY_TO_CALL; |
| mInviteReceived = null; |
| mDialog = null; |
| mServerTransaction = null; |
| mClientTransaction = null; |
| mPeerSessionDescription = null; |
| } |
| |
| public boolean isInCall() { |
| return mInCall; |
| } |
| |
| public String getLocalIp() { |
| return mLocalIp; |
| } |
| |
| public SipProfile getLocalProfile() { |
| return mLocalProfile; |
| } |
| |
| public SipProfile getPeerProfile() { |
| return mPeerProfile; |
| } |
| |
| public String getCallId() { |
| return SipHelper.getCallId(getTransaction()); |
| } |
| |
| private Transaction getTransaction() { |
| if (mClientTransaction != null) return mClientTransaction; |
| if (mServerTransaction != null) return mServerTransaction; |
| return null; |
| } |
| |
| public String getState() { |
| return mState.toString(); |
| } |
| |
| public void setListener(ISipSessionListener listener) { |
| mProxy.setListener((listener instanceof SipSessionListenerProxy) |
| ? ((SipSessionListenerProxy) listener).getListener() |
| : listener); |
| } |
| |
| // process the command in a new thread |
| private void doCommandAsync(final EventObject command) { |
| new Thread(new Runnable() { |
| public void run() { |
| try { |
| processCommand(command); |
| } catch (SipException e) { |
| Log.w(TAG, "command error: " + command, e); |
| onError(e); |
| } |
| } |
| }).start(); |
| } |
| |
| public void makeCall(SipProfile peerProfile, |
| String sessionDescription) { |
| doCommandAsync( |
| new MakeCallCommand(peerProfile, sessionDescription)); |
| } |
| |
| public void answerCall(String sessionDescription) { |
| try { |
| processCommand( |
| new MakeCallCommand(mPeerProfile, sessionDescription)); |
| } catch (SipException e) { |
| onError(e); |
| } |
| } |
| |
| public void endCall() { |
| doCommandAsync(END_CALL); |
| } |
| |
| public void changeCall(String sessionDescription) { |
| doCommandAsync( |
| new MakeCallCommand(mPeerProfile, sessionDescription)); |
| } |
| |
| public void register(int duration) { |
| doCommandAsync(new RegisterCommand(duration)); |
| } |
| |
| public void unregister() { |
| doCommandAsync(DEREGISTER); |
| } |
| |
| public boolean isReRegisterRequired() { |
| return mReRegisterFlag; |
| } |
| |
| public void clearReRegisterRequired() { |
| mReRegisterFlag = false; |
| } |
| |
| public void sendKeepAlive() { |
| mState = SipSessionState.PINGING; |
| try { |
| processCommand(new OptionsCommand()); |
| while (SipSessionState.PINGING.equals(mState)) { |
| Thread.sleep(1000); |
| } |
| } catch (SipException e) { |
| Log.e(TAG, "sendKeepAlive failed", e); |
| } catch (InterruptedException e) { |
| Log.e(TAG, "sendKeepAlive interrupted", e); |
| } |
| } |
| |
| private void processCommand(EventObject command) throws SipException { |
| if (!process(command)) { |
| onError(SipErrorCode.IN_PROGRESS, |
| "cannot initiate a new transaction to execute: " |
| + command); |
| } |
| } |
| |
| protected String generateTag() { |
| // 32-bit randomness |
| return String.valueOf((long) (Math.random() * 0x100000000L)); |
| } |
| |
| public String toString() { |
| try { |
| String s = super.toString(); |
| return s.substring(s.indexOf("@")) + ":" + mState; |
| } catch (Throwable e) { |
| return super.toString(); |
| } |
| } |
| |
| public boolean process(EventObject evt) throws SipException { |
| Log.d(TAG, " ~~~~~ " + this + ": " + mState + ": processing " |
| + log(evt)); |
| synchronized (SipSessionGroup.this) { |
| if (isClosed()) return false; |
| |
| Dialog dialog = null; |
| if (evt instanceof RequestEvent) { |
| dialog = ((RequestEvent) evt).getDialog(); |
| } else if (evt instanceof ResponseEvent) { |
| dialog = ((ResponseEvent) evt).getDialog(); |
| } |
| if (dialog != null) mDialog = dialog; |
| |
| boolean processed; |
| |
| switch (mState) { |
| case REGISTERING: |
| case DEREGISTERING: |
| processed = registeringToReady(evt); |
| break; |
| case PINGING: |
| processed = keepAliveProcess(evt); |
| break; |
| case READY_TO_CALL: |
| processed = readyForCall(evt); |
| break; |
| case INCOMING_CALL: |
| processed = incomingCall(evt); |
| break; |
| case INCOMING_CALL_ANSWERING: |
| processed = incomingCallToInCall(evt); |
| break; |
| case OUTGOING_CALL: |
| case OUTGOING_CALL_RING_BACK: |
| processed = outgoingCall(evt); |
| break; |
| case OUTGOING_CALL_CANCELING: |
| processed = outgoingCallToReady(evt); |
| break; |
| case IN_CALL: |
| processed = inCall(evt); |
| break; |
| default: |
| processed = false; |
| } |
| return (processed || processExceptions(evt)); |
| } |
| } |
| |
| private boolean processExceptions(EventObject evt) throws SipException { |
| if (isRequestEvent(Request.BYE, evt)) { |
| // terminate the call whenever a BYE is received |
| mSipHelper.sendResponse((RequestEvent) evt, Response.OK); |
| endCallNormally(); |
| return true; |
| } else if (isRequestEvent(Request.CANCEL, evt)) { |
| mSipHelper.sendResponse((RequestEvent) evt, |
| Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST); |
| return true; |
| } else if (evt instanceof TransactionTerminatedEvent) { |
| if (evt instanceof TimeoutEvent) { |
| processTimeout((TimeoutEvent) evt); |
| } else { |
| processTransactionTerminated( |
| (TransactionTerminatedEvent) evt); |
| } |
| return true; |
| } else if (evt instanceof DialogTerminatedEvent) { |
| processDialogTerminated((DialogTerminatedEvent) evt); |
| return true; |
| } |
| return false; |
| } |
| |
| private void processDialogTerminated(DialogTerminatedEvent event) { |
| if (mDialog == event.getDialog()) { |
| onError(new SipException("dialog terminated")); |
| } else { |
| Log.d(TAG, "not the current dialog; current=" + mDialog |
| + ", terminated=" + event.getDialog()); |
| } |
| } |
| |
| private void processTransactionTerminated( |
| TransactionTerminatedEvent event) { |
| switch (mState) { |
| case IN_CALL: |
| case READY_TO_CALL: |
| Log.d(TAG, "Transaction terminated; do nothing"); |
| break; |
| default: |
| Log.d(TAG, "Transaction terminated early: " + this); |
| onError(SipErrorCode.TRANSACTION_TERMINTED, |
| "transaction terminated"); |
| } |
| } |
| |
| private void processTimeout(TimeoutEvent event) { |
| Log.d(TAG, "processing Timeout..." + event); |
| Transaction current = event.isServerTransaction() |
| ? mServerTransaction |
| : mClientTransaction; |
| Transaction target = event.isServerTransaction() |
| ? event.getServerTransaction() |
| : event.getClientTransaction(); |
| |
| if ((current != target) && (mState != SipSessionState.PINGING)) { |
| Log.d(TAG, "not the current transaction; current=" + current |
| + ", timed out=" + target); |
| return; |
| } |
| switch (mState) { |
| case REGISTERING: |
| case DEREGISTERING: |
| reset(); |
| mProxy.onRegistrationTimeout(this); |
| break; |
| case INCOMING_CALL: |
| case INCOMING_CALL_ANSWERING: |
| case OUTGOING_CALL: |
| case OUTGOING_CALL_CANCELING: |
| onError(SipErrorCode.TIME_OUT, event.toString()); |
| break; |
| case PINGING: |
| reset(); |
| mReRegisterFlag = true; |
| mState = SipSessionState.READY_TO_CALL; |
| break; |
| |
| default: |
| Log.d(TAG, " do nothing"); |
| break; |
| } |
| } |
| |
| private int getExpiryTime(Response response) { |
| int expires = EXPIRY_TIME; |
| ExpiresHeader expiresHeader = (ExpiresHeader) |
| response.getHeader(ExpiresHeader.NAME); |
| if (expiresHeader != null) expires = expiresHeader.getExpires(); |
| expiresHeader = (ExpiresHeader) |
| response.getHeader(MinExpiresHeader.NAME); |
| if (expiresHeader != null) { |
| expires = Math.max(expires, expiresHeader.getExpires()); |
| } |
| return expires; |
| } |
| |
| private boolean keepAliveProcess(EventObject evt) throws SipException { |
| if (evt instanceof OptionsCommand) { |
| mClientTransaction = mSipHelper.sendKeepAlive(mLocalProfile, |
| generateTag()); |
| mDialog = mClientTransaction.getDialog(); |
| addSipSession(this); |
| return true; |
| } else if (evt instanceof ResponseEvent) { |
| return parseOptionsResult(evt); |
| } |
| return false; |
| } |
| |
| private boolean parseOptionsResult(EventObject evt) { |
| if (expectResponse(Request.OPTIONS, evt)) { |
| ResponseEvent event = (ResponseEvent) evt; |
| int rPort = getRPortFromResponse(event.getResponse()); |
| if (rPort != -1) { |
| if (mRPort == 0) mRPort = rPort; |
| if (mRPort != rPort) { |
| mReRegisterFlag = true; |
| Log.w(TAG, String.format("rport is changed: %d <> %d", |
| mRPort, rPort)); |
| mRPort = rPort; |
| } else { |
| Log.w(TAG, "rport is the same: " + rPort); |
| } |
| } else { |
| Log.w(TAG, "peer did not respect our rport request"); |
| } |
| reset(); |
| return true; |
| } |
| return false; |
| } |
| |
| private int getRPortFromResponse(Response response) { |
| ViaHeader viaHeader = (ViaHeader)(response.getHeader( |
| SIPHeaderNames.VIA)); |
| return (viaHeader == null) ? -1 : viaHeader.getRPort(); |
| } |
| |
| private boolean registeringToReady(EventObject evt) |
| throws SipException { |
| if (expectResponse(Request.REGISTER, evt)) { |
| ResponseEvent event = (ResponseEvent) evt; |
| Response response = event.getResponse(); |
| |
| int statusCode = response.getStatusCode(); |
| switch (statusCode) { |
| case Response.OK: |
| SipSessionState state = mState; |
| onRegistrationDone((state == SipSessionState.REGISTERING) |
| ? getExpiryTime(((ResponseEvent) evt).getResponse()) |
| : -1); |
| mLastNonce = null; |
| mRPort = 0; |
| return true; |
| case Response.UNAUTHORIZED: |
| case Response.PROXY_AUTHENTICATION_REQUIRED: |
| if (!handleAuthentication(event)) { |
| Log.v(TAG, "Incorrect username/password"); |
| onRegistrationFailed(SipErrorCode.INVALID_CREDENTIALS, |
| "incorrect username or password"); |
| } |
| return true; |
| default: |
| if (statusCode >= 500) { |
| onRegistrationFailed(response); |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private boolean handleAuthentication(ResponseEvent event) |
| throws SipException { |
| Response response = event.getResponse(); |
| String nonce = getNonceFromResponse(response); |
| if (((nonce != null) && nonce.equals(mLastNonce)) || |
| (nonce == mLastNonce)) { |
| return false; |
| } else { |
| mClientTransaction = mSipHelper.handleChallenge( |
| event, getAccountManager()); |
| mDialog = mClientTransaction.getDialog(); |
| mLastNonce = nonce; |
| return true; |
| } |
| } |
| |
| private AccountManager getAccountManager() { |
| return new AccountManager() { |
| public UserCredentials getCredentials(ClientTransaction |
| challengedTransaction, String realm) { |
| return new UserCredentials() { |
| public String getUserName() { |
| return mLocalProfile.getUserName(); |
| } |
| |
| public String getPassword() { |
| return mPassword; |
| } |
| |
| public String getSipDomain() { |
| return mLocalProfile.getSipDomain(); |
| } |
| }; |
| } |
| }; |
| } |
| |
| private String getNonceFromResponse(Response response) { |
| WWWAuthenticate authHeader = (WWWAuthenticate)(response.getHeader( |
| SIPHeaderNames.WWW_AUTHENTICATE)); |
| return (authHeader == null) ? null : authHeader.getNonce(); |
| } |
| |
| private boolean readyForCall(EventObject evt) throws SipException { |
| // expect MakeCallCommand, RegisterCommand, DEREGISTER |
| if (evt instanceof MakeCallCommand) { |
| MakeCallCommand cmd = (MakeCallCommand) evt; |
| mPeerProfile = cmd.getPeerProfile(); |
| mClientTransaction = mSipHelper.sendInvite(mLocalProfile, |
| mPeerProfile, cmd.getSessionDescription(), |
| generateTag()); |
| mDialog = mClientTransaction.getDialog(); |
| addSipSession(this); |
| mState = SipSessionState.OUTGOING_CALL; |
| mProxy.onCalling(this); |
| return true; |
| } else if (evt instanceof RegisterCommand) { |
| int duration = ((RegisterCommand) evt).getDuration(); |
| mClientTransaction = mSipHelper.sendRegister(mLocalProfile, |
| generateTag(), duration); |
| mDialog = mClientTransaction.getDialog(); |
| addSipSession(this); |
| mState = SipSessionState.REGISTERING; |
| mProxy.onRegistering(this); |
| return true; |
| } else if (DEREGISTER == evt) { |
| mClientTransaction = mSipHelper.sendRegister(mLocalProfile, |
| generateTag(), 0); |
| mDialog = mClientTransaction.getDialog(); |
| addSipSession(this); |
| mState = SipSessionState.DEREGISTERING; |
| mProxy.onRegistering(this); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean incomingCall(EventObject evt) throws SipException { |
| // expect MakeCallCommand(answering) , END_CALL cmd , Cancel |
| if (evt instanceof MakeCallCommand) { |
| // answer call |
| mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived, |
| mLocalProfile, |
| ((MakeCallCommand) evt).getSessionDescription(), |
| mServerTransaction); |
| mState = SipSessionState.INCOMING_CALL_ANSWERING; |
| return true; |
| } else if (END_CALL == evt) { |
| mSipHelper.sendInviteBusyHere(mInviteReceived, |
| mServerTransaction); |
| endCallNormally(); |
| return true; |
| } else if (isRequestEvent(Request.CANCEL, evt)) { |
| RequestEvent event = (RequestEvent) evt; |
| mSipHelper.sendResponse(event, Response.OK); |
| mSipHelper.sendInviteRequestTerminated( |
| mInviteReceived.getRequest(), mServerTransaction); |
| endCallNormally(); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean incomingCallToInCall(EventObject evt) |
| throws SipException { |
| // expect ACK, CANCEL request |
| if (isRequestEvent(Request.ACK, evt)) { |
| establishCall(); |
| return true; |
| } else if (isRequestEvent(Request.CANCEL, evt)) { |
| // http://tools.ietf.org/html/rfc3261#section-9.2 |
| // Final response has been sent; do nothing here. |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean outgoingCall(EventObject evt) throws SipException { |
| if (expectResponse(Request.INVITE, evt)) { |
| ResponseEvent event = (ResponseEvent) evt; |
| Response response = event.getResponse(); |
| |
| int statusCode = response.getStatusCode(); |
| switch (statusCode) { |
| case Response.RINGING: |
| if (mState == SipSessionState.OUTGOING_CALL) { |
| mState = SipSessionState.OUTGOING_CALL_RING_BACK; |
| mProxy.onRingingBack(this); |
| } |
| return true; |
| case Response.OK: |
| mSipHelper.sendInviteAck(event, mDialog); |
| mPeerSessionDescription = extractContent(response); |
| establishCall(); |
| return true; |
| case Response.PROXY_AUTHENTICATION_REQUIRED: |
| if (handleAuthentication(event)) { |
| addSipSession(this); |
| } else { |
| endCallOnError(SipErrorCode.INVALID_CREDENTIALS, |
| "incorrect username or password"); |
| } |
| return true; |
| case Response.REQUEST_PENDING: |
| // TODO: |
| // rfc3261#section-14.1; re-schedule invite |
| return true; |
| default: |
| if (statusCode >= 400) { |
| // error: an ack is sent automatically by the stack |
| onError(response); |
| return true; |
| } else if (statusCode >= 300) { |
| // TODO: handle 3xx (redirect) |
| } else { |
| return true; |
| } |
| } |
| return false; |
| } else if (END_CALL == evt) { |
| // RFC says that UA should not send out cancel when no |
| // response comes back yet. We are cheating for not checking |
| // response. |
| mSipHelper.sendCancel(mClientTransaction); |
| mState = SipSessionState.OUTGOING_CALL_CANCELING; |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean outgoingCallToReady(EventObject evt) |
| throws SipException { |
| if (evt instanceof ResponseEvent) { |
| ResponseEvent event = (ResponseEvent) evt; |
| Response response = event.getResponse(); |
| int statusCode = response.getStatusCode(); |
| if (expectResponse(Request.CANCEL, evt)) { |
| if (statusCode == Response.OK) { |
| // do nothing; wait for REQUEST_TERMINATED |
| return true; |
| } |
| } else if (expectResponse(Request.INVITE, evt)) { |
| if (statusCode == Response.OK) { |
| outgoingCall(evt); // abort Cancel |
| return true; |
| } |
| } else { |
| return false; |
| } |
| |
| if (statusCode >= 400) { |
| onError(response); |
| return true; |
| } |
| } else if (evt instanceof TransactionTerminatedEvent) { |
| // rfc3261#section-14.1: |
| // if re-invite gets timed out, terminate the dialog; but |
| // re-invite is not reliable, just let it go and pretend |
| // nothing happened. |
| onError(new SipException("timed out")); |
| } |
| return false; |
| } |
| |
| private boolean inCall(EventObject evt) throws SipException { |
| // expect END_CALL cmd, BYE request, hold call (MakeCallCommand) |
| // OK retransmission is handled in SipStack |
| if (END_CALL == evt) { |
| // rfc3261#section-15.1.1 |
| mSipHelper.sendBye(mDialog); |
| endCallNormally(); |
| return true; |
| } else if (isRequestEvent(Request.INVITE, evt)) { |
| // got Re-INVITE |
| RequestEvent event = mInviteReceived = (RequestEvent) evt; |
| mState = SipSessionState.INCOMING_CALL; |
| mPeerSessionDescription = extractContent(event.getRequest()); |
| mServerTransaction = null; |
| mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription); |
| return true; |
| } else if (isRequestEvent(Request.BYE, evt)) { |
| mSipHelper.sendResponse((RequestEvent) evt, Response.OK); |
| endCallNormally(); |
| return true; |
| } else if (evt instanceof MakeCallCommand) { |
| // to change call |
| mClientTransaction = mSipHelper.sendReinvite(mDialog, |
| ((MakeCallCommand) evt).getSessionDescription()); |
| mState = SipSessionState.OUTGOING_CALL; |
| return true; |
| } |
| return false; |
| } |
| |
| private String createErrorMessage(Response response) { |
| return String.format(SERVER_ERROR_PREFIX + "%s (%d)", |
| response.getReasonPhrase(), response.getStatusCode()); |
| } |
| |
| private void establishCall() { |
| mState = SipSessionState.IN_CALL; |
| mInCall = true; |
| mProxy.onCallEstablished(this, mPeerSessionDescription); |
| } |
| |
| private void fallbackToPreviousInCall(Throwable exception) { |
| exception = getRootCause(exception); |
| fallbackToPreviousInCall(getErrorCode(exception), |
| exception.toString()); |
| } |
| |
| private void fallbackToPreviousInCall(SipErrorCode errorCode, |
| String message) { |
| mState = SipSessionState.IN_CALL; |
| mProxy.onCallChangeFailed(this, errorCode.toString(), message); |
| } |
| |
| private void endCallNormally() { |
| reset(); |
| mProxy.onCallEnded(this); |
| } |
| |
| private void endCallOnError(SipErrorCode errorCode, String message) { |
| reset(); |
| mProxy.onError(this, errorCode.toString(), message); |
| } |
| |
| private void endCallOnBusy() { |
| reset(); |
| mProxy.onCallBusy(this); |
| } |
| |
| private void onError(SipErrorCode errorCode, String message) { |
| switch (mState) { |
| case REGISTERING: |
| case DEREGISTERING: |
| onRegistrationFailed(errorCode, message); |
| break; |
| default: |
| if (mInCall) { |
| fallbackToPreviousInCall(errorCode, message); |
| } else { |
| endCallOnError(errorCode, message); |
| } |
| } |
| } |
| |
| |
| private void onError(Throwable exception) { |
| exception = getRootCause(exception); |
| onError(getErrorCode(exception), exception.toString()); |
| } |
| |
| private void onError(Response response) { |
| int statusCode = response.getStatusCode(); |
| if (!mInCall && ((statusCode == Response.TEMPORARILY_UNAVAILABLE) |
| || (statusCode == Response.BUSY_HERE))) { |
| endCallOnBusy(); |
| } else { |
| onError(getErrorCode(statusCode), createErrorMessage(response)); |
| } |
| } |
| |
| private SipErrorCode getErrorCode(int responseStatusCode) { |
| switch (responseStatusCode) { |
| case Response.NOT_FOUND: |
| case Response.ADDRESS_INCOMPLETE: |
| return SipErrorCode.INVALID_REMOTE_URI; |
| case Response.REQUEST_TIMEOUT: |
| return SipErrorCode.TIME_OUT; |
| default: |
| if (responseStatusCode < 500) { |
| return SipErrorCode.CLIENT_ERROR; |
| } else { |
| return SipErrorCode.SERVER_ERROR; |
| } |
| } |
| } |
| |
| private Throwable getRootCause(Throwable exception) { |
| Throwable cause = exception.getCause(); |
| while (cause != null) { |
| exception = cause; |
| cause = exception.getCause(); |
| } |
| return exception; |
| } |
| |
| private SipErrorCode getErrorCode(Throwable exception) { |
| String message = exception.getMessage(); |
| if (exception instanceof UnknownHostException) { |
| return SipErrorCode.INVALID_REMOTE_URI; |
| } else if (exception instanceof IOException) { |
| return SipErrorCode.SOCKET_ERROR; |
| } else if (message.startsWith(SERVER_ERROR_PREFIX)) { |
| return SipErrorCode.SERVER_ERROR; |
| } else { |
| return SipErrorCode.CLIENT_ERROR; |
| } |
| } |
| |
| private void onRegistrationDone(int duration) { |
| reset(); |
| mProxy.onRegistrationDone(this, duration); |
| } |
| |
| private void onRegistrationFailed(SipErrorCode errorCode, |
| String message) { |
| reset(); |
| mProxy.onRegistrationFailed(this, errorCode.toString(), message); |
| } |
| |
| private void onRegistrationFailed(Throwable exception) { |
| reset(); |
| exception = getRootCause(exception); |
| onRegistrationFailed(getErrorCode(exception), |
| exception.toString()); |
| } |
| |
| private void onRegistrationFailed(Response response) { |
| reset(); |
| int statusCode = response.getStatusCode(); |
| onRegistrationFailed(getErrorCode(statusCode), |
| createErrorMessage(response)); |
| } |
| } |
| |
| /** |
| * @return true if the event is a request event matching the specified |
| * method; false otherwise |
| */ |
| private static boolean isRequestEvent(String method, EventObject event) { |
| try { |
| if (event instanceof RequestEvent) { |
| RequestEvent requestEvent = (RequestEvent) event; |
| return method.equals(requestEvent.getRequest().getMethod()); |
| } |
| } catch (Throwable e) { |
| } |
| return false; |
| } |
| |
| private static String getCseqMethod(Message message) { |
| return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod(); |
| } |
| |
| /** |
| * @return true if the event is a response event and the CSeqHeader method |
| * match the given arguments; false otherwise |
| */ |
| private static boolean expectResponse( |
| String expectedMethod, EventObject evt) { |
| if (evt instanceof ResponseEvent) { |
| ResponseEvent event = (ResponseEvent) evt; |
| Response response = event.getResponse(); |
| return expectedMethod.equalsIgnoreCase(getCseqMethod(response)); |
| } |
| return false; |
| } |
| |
| /** |
| * @return true if the event is a response event and the response code and |
| * CSeqHeader method match the given arguments; false otherwise |
| */ |
| private static boolean expectResponse( |
| int responseCode, String expectedMethod, EventObject evt) { |
| if (evt instanceof ResponseEvent) { |
| ResponseEvent event = (ResponseEvent) evt; |
| Response response = event.getResponse(); |
| if (response.getStatusCode() == responseCode) { |
| return expectedMethod.equalsIgnoreCase(getCseqMethod(response)); |
| } |
| } |
| return false; |
| } |
| |
| private static SipProfile createPeerProfile(Request request) |
| throws SipException { |
| try { |
| FromHeader fromHeader = |
| (FromHeader) request.getHeader(FromHeader.NAME); |
| Address address = fromHeader.getAddress(); |
| SipURI uri = (SipURI) address.getURI(); |
| String username = uri.getUser(); |
| if (username == null) username = ANONYMOUS; |
| return new SipProfile.Builder(username, uri.getHost()) |
| .setPort(uri.getPort()) |
| .setDisplayName(address.getDisplayName()) |
| .build(); |
| } catch (InvalidArgumentException e) { |
| throw new SipException("createPeerProfile()", e); |
| } catch (ParseException e) { |
| throw new SipException("createPeerProfile()", e); |
| } |
| } |
| |
| private static String log(EventObject evt) { |
| if (evt instanceof RequestEvent) { |
| return ((RequestEvent) evt).getRequest().toString(); |
| } else if (evt instanceof ResponseEvent) { |
| return ((ResponseEvent) evt).getResponse().toString(); |
| } else { |
| return evt.toString(); |
| } |
| } |
| |
| private class OptionsCommand extends EventObject { |
| public OptionsCommand() { |
| super(SipSessionGroup.this); |
| } |
| } |
| |
| private class RegisterCommand extends EventObject { |
| private int mDuration; |
| |
| public RegisterCommand(int duration) { |
| super(SipSessionGroup.this); |
| mDuration = duration; |
| } |
| |
| public int getDuration() { |
| return mDuration; |
| } |
| } |
| |
| private class MakeCallCommand extends EventObject { |
| private String mSessionDescription; |
| |
| public MakeCallCommand(SipProfile peerProfile, |
| String sessionDescription) { |
| super(peerProfile); |
| mSessionDescription = sessionDescription; |
| } |
| |
| public SipProfile getPeerProfile() { |
| return (SipProfile) getSource(); |
| } |
| |
| public String getSessionDescription() { |
| return mSessionDescription; |
| } |
| } |
| |
| } |