Add REFER handling.

Handle REFER requests including REFER with Replaces header.

bug:4958680
Change-Id: I96df95097b78bed67ab8abd309a1e57a45c6bc2f
diff --git a/java/android/net/sip/SipAudioCall.java b/java/android/net/sip/SipAudioCall.java
index c1affa6..fcdbd2c 100644
--- a/java/android/net/sip/SipAudioCall.java
+++ b/java/android/net/sip/SipAudioCall.java
@@ -57,6 +57,7 @@
     private static final boolean RELEASE_SOCKET = true;
     private static final boolean DONT_RELEASE_SOCKET = false;
     private static final int SESSION_TIMEOUT = 5; // in seconds
+    private static final int TRANSFER_TIMEOUT = 15; // in seconds
 
     /** Listener for events relating to a SIP call, such as when a call is being
      * recieved ("on ringing") or a call is outgoing ("on calling").
@@ -537,10 +538,14 @@
                 Log.v(TAG, "onCallTransferring mSipSession:"
                         + mSipSession + " newSession:" + newSession);
                 mTransferringSession = newSession;
-                // session changing request
                 try {
-                    String answer = createAnswer(sessionDescription).encode();
-                    newSession.answerCall(answer, SESSION_TIMEOUT);
+                    if (sessionDescription == null) {
+                        newSession.makeCall(newSession.getPeerProfile(),
+                                createOffer().encode(), TRANSFER_TIMEOUT);
+                    } else {
+                        String answer = createAnswer(sessionDescription).encode();
+                        newSession.answerCall(answer, SESSION_TIMEOUT);
+                    }
                 } catch (Throwable e) {
                     Log.e(TAG, "onCallTransferring()", e);
                     newSession.endCall();
diff --git a/java/com/android/server/sip/SipHelper.java b/java/com/android/server/sip/SipHelper.java
index c031bc1..dc628e0 100644
--- a/java/com/android/server/sip/SipHelper.java
+++ b/java/com/android/server/sip/SipHelper.java
@@ -19,6 +19,9 @@
 import gov.nist.javax.sip.SipStackExt;
 import gov.nist.javax.sip.clientauthutils.AccountManager;
 import gov.nist.javax.sip.clientauthutils.AuthenticationHelper;
+import gov.nist.javax.sip.header.extensions.ReferencesHeader;
+import gov.nist.javax.sip.header.extensions.ReferredByHeader;
+import gov.nist.javax.sip.header.extensions.ReplacesHeader;
 
 import android.net.sip.SipProfile;
 import android.util.Log;
@@ -284,14 +287,18 @@
     }
 
     public ClientTransaction sendInvite(SipProfile caller, SipProfile callee,
-            String sessionDescription, String tag)
-            throws SipException {
+            String sessionDescription, String tag, ReferredByHeader referredBy,
+            String replaces) throws SipException {
         try {
             Request request = createRequest(Request.INVITE, caller, callee, tag);
+            if (referredBy != null) request.addHeader(referredBy);
+            if (replaces != null) {
+                request.addHeader(mHeaderFactory.createHeader(
+                        ReplacesHeader.NAME, replaces));
+            }
             request.setContent(sessionDescription,
                     mHeaderFactory.createContentTypeHeader(
                             "application", "sdp"));
-
             ClientTransaction clientTransaction =
                     mSipProvider.getNewClientTransaction(request);
             if (DEBUG) Log.d(TAG, "send INVITE: " + request);
@@ -455,6 +462,25 @@
         }
     }
 
+    public void sendReferNotify(Dialog dialog, String content)
+            throws SipException {
+        try {
+            Request request = dialog.createRequest(Request.NOTIFY);
+            request.addHeader(mHeaderFactory.createSubscriptionStateHeader(
+                    "active;expires=60"));
+            // set content here
+            request.setContent(content,
+                    mHeaderFactory.createContentTypeHeader(
+                            "message", "sipfrag"));
+            request.addHeader(mHeaderFactory.createEventHeader(
+                    ReferencesHeader.REFER));
+            if (DEBUG) Log.d(TAG, "send NOTIFY: " + request);
+            dialog.sendRequest(mSipProvider.getNewClientTransaction(request));
+        } catch (ParseException e) {
+            throw new SipException("sendReferNotify()", e);
+        }
+    }
+
     public void sendInviteRequestTerminated(Request inviteRequest,
             ServerTransaction inviteTransaction) throws SipException {
         try {
diff --git a/java/com/android/server/sip/SipSessionGroup.java b/java/com/android/server/sip/SipSessionGroup.java
index 4e44402..48d9b17 100644
--- a/java/com/android/server/sip/SipSessionGroup.java
+++ b/java/com/android/server/sip/SipSessionGroup.java
@@ -18,12 +18,15 @@
 
 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.ProxyAuthenticate;
+import gov.nist.javax.sip.header.ReferTo;
+import gov.nist.javax.sip.header.SIPHeaderNames;
+import gov.nist.javax.sip.header.StatusLine;
 import gov.nist.javax.sip.header.WWWAuthenticate;
 import gov.nist.javax.sip.header.extensions.ReferredByHeader;
 import gov.nist.javax.sip.header.extensions.ReplacesHeader;
 import gov.nist.javax.sip.message.SIPMessage;
+import gov.nist.javax.sip.message.SIPResponse;
 
 import android.net.sip.ISipSession;
 import android.net.sip.ISipSessionListener;
@@ -71,12 +74,15 @@
 import javax.sip.header.CSeqHeader;
 import javax.sip.header.ExpiresHeader;
 import javax.sip.header.FromHeader;
+import javax.sip.header.HeaderAddress;
 import javax.sip.header.MinExpiresHeader;
+import javax.sip.header.ReferToHeader;
 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.
  */
@@ -390,25 +396,26 @@
         }
     }
 
+    private SipSessionImpl createNewSession(RequestEvent event,
+            ISipSessionListener listener, ServerTransaction transaction,
+            int newState) throws SipException {
+        SipSessionImpl newSession = new SipSessionImpl(listener);
+        newSession.mServerTransaction = transaction;
+        newSession.mState = newState;
+        newSession.mDialog = newSession.mServerTransaction.getDialog();
+        newSession.mInviteReceived = event;
+        newSession.mPeerProfile = createPeerProfile((HeaderAddress)
+                event.getRequest().getHeader(FromHeader.NAME));
+        newSession.mPeerSessionDescription =
+                extractContent(event.getRequest());
+        return newSession;
+    }
+
     private class SipSessionCallReceiverImpl extends SipSessionImpl {
         public SipSessionCallReceiverImpl(ISipSessionListener listener) {
             super(listener);
         }
 
-        private SipSessionImpl createNewSession(RequestEvent event,
-                ISipSessionListener listener, ServerTransaction transaction)
-                throws SipException {
-            SipSessionImpl newSession = new SipSessionImpl(listener);
-            newSession.mServerTransaction = transaction;
-            newSession.mState = SipSession.State.INCOMING_CALL;
-            newSession.mDialog = newSession.mServerTransaction.getDialog();
-            newSession.mInviteReceived = event;
-            newSession.mPeerProfile = createPeerProfile(event.getRequest());
-            newSession.mPeerSessionDescription =
-                    extractContent(event.getRequest());
-            return newSession;
-        }
-
         private int processInviteWithReplaces(RequestEvent event,
                 ReplacesHeader replaces) {
             String callId = replaces.getCallId();
@@ -452,7 +459,8 @@
                     // got INVITE w/ replaces request.
                     newSession = createNewSession(event,
                             replacedSession.mProxy.getListener(),
-                            mSipHelper.getServerTransaction(event));
+                            mSipHelper.getServerTransaction(event),
+                            SipSession.State.INCOMING_CALL);
                     newSession.mProxy.onCallTransferring(newSession,
                             newSession.mPeerSessionDescription);
                 } else {
@@ -461,7 +469,8 @@
             } else {
                 // New Incoming call.
                 newSession = createNewSession(event, mProxy,
-                        mSipHelper.sendRinging(event, generateTag()));
+                        mSipHelper.sendRinging(event, generateTag()),
+                        SipSession.State.INCOMING_CALL);
                 mProxy.onRinging(newSession, newSession.mPeerProfile,
                         newSession.mPeerSessionDescription);
             }
@@ -507,6 +516,11 @@
 
         private SipSessionImpl mKeepAliveSession;
 
+        // the following three members are used for handling refer request.
+        SipSessionImpl mReferSession;
+        ReferredByHeader mReferredBy;
+        String mReplaces;
+
         // lightweight timer
         class SessionTimer {
             private boolean mRunning = true;
@@ -556,6 +570,9 @@
             mInviteReceived = null;
             mPeerSessionDescription = null;
             mAuthenticationRetryCount = 0;
+            mReferSession = null;
+            mReferredBy = null;
+            mReplaces = null;
 
             if (mDialog != null) mDialog.delete();
             mDialog = null;
@@ -969,15 +986,26 @@
             return (proxyAuth == null) ? null : proxyAuth.getNonce();
         }
 
+        private String getResponseString(int statusCode) {
+            StatusLine statusLine = new StatusLine();
+            statusLine.setStatusCode(statusCode);
+            statusLine.setReasonPhrase(SIPResponse.getReasonPhrase(statusCode));
+            return statusLine.encode();
+        }
+
         private boolean readyForCall(EventObject evt) throws SipException {
             // expect MakeCallCommand, RegisterCommand, DEREGISTER
             if (evt instanceof MakeCallCommand) {
                 mState = SipSession.State.OUTGOING_CALL;
                 MakeCallCommand cmd = (MakeCallCommand) evt;
                 mPeerProfile = cmd.getPeerProfile();
-                mClientTransaction = mSipHelper.sendInvite(mLocalProfile,
-                        mPeerProfile, cmd.getSessionDescription(),
-                        generateTag());
+                if (mReferSession != null) {
+                    mSipHelper.sendReferNotify(mReferSession.mDialog,
+                            getResponseString(Response.TRYING));
+                }
+                mClientTransaction = mSipHelper.sendInvite(
+                        mLocalProfile, mPeerProfile, cmd.getSessionDescription(),
+                        generateTag(), mReferredBy, mReplaces);
                 mDialog = mClientTransaction.getDialog();
                 addSipSession(this);
                 startSessionTimer(cmd.getTimeout());
@@ -1072,6 +1100,12 @@
                     }
                     return true;
                 case Response.OK:
+                    if (mReferSession != null) {
+                        mSipHelper.sendReferNotify(mReferSession.mDialog,
+                                getResponseString(Response.OK));
+                        // since we don't need to remember the session anymore.
+                        mReferSession = null;
+                    }
                     mSipHelper.sendInviteAck(event, mDialog);
                     mPeerSessionDescription = extractContent(response);
                     establishCall(true);
@@ -1087,6 +1121,10 @@
                     // rfc3261#section-14.1; re-schedule invite
                     return true;
                 default:
+                    if (mReferSession != null) {
+                        mSipHelper.sendReferNotify(mReferSession.mDialog,
+                                getResponseString(Response.SERVICE_UNAVAILABLE));
+                    }
                     if (statusCode >= 400) {
                         // error: an ack is sent automatically by the stack
                         onError(response);
@@ -1155,6 +1193,38 @@
             return false;
         }
 
+        private boolean processReferRequest(RequestEvent event)
+                throws SipException {
+            try {
+                ReferToHeader referto = (ReferToHeader) event.getRequest()
+                        .getHeader(ReferTo.NAME);
+                Address address = referto.getAddress();
+                SipURI uri = (SipURI) address.getURI();
+                String replacesHeader = uri.getHeader(ReplacesHeader.NAME);
+                String username = uri.getUser();
+                if (username == null) {
+                    mSipHelper.sendResponse(event, Response.BAD_REQUEST);
+                    return false;
+                }
+                // send notify accepted
+                mSipHelper.sendResponse(event, Response.ACCEPTED);
+                SipSessionImpl newSession = createNewSession(event,
+                        this.mProxy.getListener(),
+                        mSipHelper.getServerTransaction(event),
+                        SipSession.State.READY_TO_CALL);
+                newSession.mReferSession = this;
+                newSession.mReferredBy = (ReferredByHeader) event.getRequest()
+                        .getHeader(ReferredByHeader.NAME);
+                newSession.mReplaces = replacesHeader;
+                newSession.mPeerProfile = createPeerProfile(referto);
+                newSession.mProxy.onCallTransferring(newSession,
+                        null);
+                return true;
+            } catch (IllegalArgumentException e) {
+                throw new SipException("createPeerProfile()", e);
+            }
+        }
+
         private boolean inCall(EventObject evt) throws SipException {
             // expect END_CALL cmd, BYE request, hold call (MakeCallCommand)
             // OK retransmission is handled in SipStack
@@ -1175,6 +1245,8 @@
                 mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
                 endCallNormally();
                 return true;
+            } else if (isRequestEvent(Request.REFER, evt)) {
+                return processReferRequest((RequestEvent) evt);
             } else if (evt instanceof MakeCallCommand) {
                 // to change call
                 mState = SipSession.State.OUTGOING_CALL;
@@ -1182,6 +1254,8 @@
                         ((MakeCallCommand) evt).getSessionDescription());
                 startSessionTimer(((MakeCallCommand) evt).getTimeout());
                 return true;
+            } else if (evt instanceof ResponseEvent) {
+                if (expectResponse(Request.NOTIFY, evt)) return true;
             }
             return false;
         }
@@ -1558,12 +1632,10 @@
         return false;
     }
 
-    private static SipProfile createPeerProfile(Request request)
+    private static SipProfile createPeerProfile(HeaderAddress header)
             throws SipException {
         try {
-            FromHeader fromHeader =
-                    (FromHeader) request.getHeader(FromHeader.NAME);
-            Address address = fromHeader.getAddress();
+            Address address = header.getAddress();
             SipURI uri = (SipURI) address.getURI();
             String username = uri.getUser();
             if (username == null) username = ANONYMOUS;