Merge "Add framework support for camcorder zoom."
diff --git a/voip/java/android/net/sip/ISipSessionListener.aidl b/voip/java/android/net/sip/ISipSessionListener.aidl
index 5920bca..690700c 100644
--- a/voip/java/android/net/sip/ISipSessionListener.aidl
+++ b/voip/java/android/net/sip/ISipSessionListener.aidl
@@ -72,6 +72,14 @@
     void onCallBusy(in ISipSession session);
 
     /**
+     * Called when the call is being transferred to a new one.
+     *
+     * @param newSession the new session that the call will be transferred to
+     * @param sessionDescription the new peer's session description
+     */
+    void onCallTransferring(in ISipSession newSession, String sessionDescription);
+
+    /**
      * Called when an error occurs during session initialization and
      * termination.
      *
diff --git a/voip/java/android/net/sip/SipAudioCall.java b/voip/java/android/net/sip/SipAudioCall.java
index b46f8268..2666b69 100644
--- a/voip/java/android/net/sip/SipAudioCall.java
+++ b/voip/java/android/net/sip/SipAudioCall.java
@@ -170,6 +170,7 @@
     private SipProfile mLocalProfile;
     private SipAudioCall.Listener mListener;
     private SipSession mSipSession;
+    private SipSession mTransferringSession;
 
     private long mSessionId = System.currentTimeMillis();
     private String mPeerSd;
@@ -347,6 +348,27 @@
         }
     }
 
+    private synchronized void transferToNewSession() {
+        if (mTransferringSession == null) return;
+        SipSession origin = mSipSession;
+        mSipSession = mTransferringSession;
+        mTransferringSession = null;
+
+        // stop the replaced call.
+        if (mAudioStream != null) {
+            mAudioStream.join(null);
+        } else {
+            try {
+                mAudioStream = new AudioStream(InetAddress.getByName(
+                        getLocalIp()));
+            } catch (Throwable t) {
+                Log.i(TAG, "transferToNewSession(): " + t);
+            }
+        }
+        if (origin != null) origin.endCall();
+        startAudio();
+    }
+
     private SipSession.Listener createListener() {
         return new SipSession.Listener() {
             @Override
@@ -404,6 +426,13 @@
                 mPeerSd = sessionDescription;
                 Log.v(TAG, "onCallEstablished()" + mPeerSd);
 
+                // TODO: how to notify the UI that the remote party is changed
+                if ((mTransferringSession != null)
+                        && (session == mTransferringSession)) {
+                    transferToNewSession();
+                    return;
+                }
+
                 Listener listener = mListener;
                 if (listener != null) {
                     try {
@@ -420,7 +449,17 @@
 
             @Override
             public void onCallEnded(SipSession session) {
-                Log.d(TAG, "sip call ended: " + session);
+                Log.d(TAG, "sip call ended: " + session + " mSipSession:" + mSipSession);
+                // reset the trasnferring session if it is the one.
+                if (session == mTransferringSession) {
+                    mTransferringSession = null;
+                    return;
+                }
+                // or ignore the event if the original session is being
+                // transferred to the new one.
+                if ((mTransferringSession != null) ||
+                    (session != mSipSession)) return;
+
                 Listener listener = mListener;
                 if (listener != null) {
                     try {
@@ -489,6 +528,22 @@
             public void onRegistrationDone(SipSession session, int duration) {
                 // irrelevant
             }
+
+            @Override
+            public void onCallTransferring(SipSession newSession,
+                    String sessionDescription) {
+                Log.v(TAG, "onCallTransferring mSipSession:"
+                        + mSipSession + " newSession:" + newSession);
+                mTransferringSession = newSession;
+                // session changing request
+                try {
+                    String answer = createAnswer(sessionDescription).encode();
+                    newSession.answerCall(answer, SESSION_TIMEOUT);
+                } catch (Throwable e) {
+                    Log.e(TAG, "onCallTransferring()", e);
+                    newSession.endCall();
+                }
+            }
         };
     }
 
diff --git a/voip/java/android/net/sip/SipSession.java b/voip/java/android/net/sip/SipSession.java
index 5629b3c..5ba1626 100644
--- a/voip/java/android/net/sip/SipSession.java
+++ b/voip/java/android/net/sip/SipSession.java
@@ -160,6 +160,17 @@
         }
 
         /**
+         * Called when the call is being transferred to a new one.
+         *
+         * @hide
+         * @param newSession the new session that the call will be transferred to
+         * @param sessionDescription the new peer's session description
+         */
+        public void onCallTransferring(SipSession newSession,
+                String sessionDescription) {
+        }
+
+        /**
          * Called when an error occurs during session initialization and
          * termination.
          *
@@ -489,6 +500,16 @@
                 }
             }
 
+            public void onCallTransferring(ISipSession session,
+                    String sessionDescription) {
+                if (mListener != null) {
+                    mListener.onCallTransferring(
+                            new SipSession(session, SipSession.this.mListener),
+                            sessionDescription);
+
+                }
+            }
+
             public void onCallChangeFailed(ISipSession session, int errorCode,
                     String message) {
                 if (mListener != null) {
diff --git a/voip/java/android/net/sip/SipSessionAdapter.java b/voip/java/android/net/sip/SipSessionAdapter.java
index 86aca37..f538983 100644
--- a/voip/java/android/net/sip/SipSessionAdapter.java
+++ b/voip/java/android/net/sip/SipSessionAdapter.java
@@ -42,6 +42,10 @@
     public void onCallBusy(ISipSession session) {
     }
 
+    public void onCallTransferring(ISipSession session,
+            String sessionDescription) {
+    }
+
     public void onCallChangeFailed(ISipSession session, int errorCode,
             String message) {
     }
diff --git a/voip/java/com/android/server/sip/SipHelper.java b/voip/java/com/android/server/sip/SipHelper.java
index 018e6de..47950e3 100644
--- a/voip/java/com/android/server/sip/SipHelper.java
+++ b/voip/java/com/android/server/sip/SipHelper.java
@@ -314,7 +314,7 @@
         }
     }
 
-    private ServerTransaction getServerTransaction(RequestEvent event)
+    public ServerTransaction getServerTransaction(RequestEvent event)
             throws SipException {
         ServerTransaction transaction = event.getServerTransaction();
         if (transaction == null) {
diff --git a/voip/java/com/android/server/sip/SipSessionGroup.java b/voip/java/com/android/server/sip/SipSessionGroup.java
index 2d0dd9c..481e306 100644
--- a/voip/java/com/android/server/sip/SipSessionGroup.java
+++ b/voip/java/com/android/server/sip/SipSessionGroup.java
@@ -21,6 +21,8 @@
 import gov.nist.javax.sip.header.SIPHeaderNames;
 import gov.nist.javax.sip.header.ProxyAuthenticate;
 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 android.net.sip.ISipSession;
@@ -365,24 +367,85 @@
             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();
+            SipSessionImpl session = mSessionMap.get(callId);
+            if (session == null) {
+                return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
+            }
+
+            Dialog dialog = session.mDialog;
+            if (dialog == null) return Response.DECLINE;
+
+            if (!dialog.getLocalTag().equals(replaces.getToTag()) ||
+                    !dialog.getRemoteTag().equals(replaces.getFromTag())) {
+                // No match is found, returns 481.
+                return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
+            }
+
+            ReferredByHeader referredBy = (ReferredByHeader) event.getRequest()
+                    .getHeader(ReferredByHeader.NAME);
+            if ((referredBy == null) ||
+                    !dialog.getRemoteParty().equals(referredBy.getAddress())) {
+                return Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST;
+            }
+            return Response.OK;
+        }
+
+        private void processNewInviteRequest(RequestEvent event)
+                throws SipException {
+            ReplacesHeader replaces = (ReplacesHeader) event.getRequest()
+                    .getHeader(ReplacesHeader.NAME);
+            SipSessionImpl newSession = null;
+            if (replaces != null) {
+                int response = processInviteWithReplaces(event, replaces);
+                if (DEBUG) {
+                    Log.v(TAG, "ReplacesHeader: " + replaces
+                            + " response=" + response);
+                }
+                if (response == Response.OK) {
+                    SipSessionImpl replacedSession =
+                            mSessionMap.get(replaces.getCallId());
+                    // got INVITE w/ replaces request.
+                    newSession = createNewSession(event,
+                            replacedSession.mProxy.getListener(),
+                            mSipHelper.getServerTransaction(event));
+                    newSession.mProxy.onCallTransferring(newSession,
+                            newSession.mPeerSessionDescription);
+                } else {
+                    mSipHelper.sendResponse(event, response);
+                }
+            } else {
+                // New Incoming call.
+                newSession = createNewSession(event, mProxy,
+                        mSipHelper.sendRinging(event, generateTag()));
+                mProxy.onRinging(newSession, newSession.mPeerProfile,
+                        newSession.mPeerSessionDescription);
+            }
+            if (newSession != null) addSipSession(newSession);
+        }
+
         public boolean process(EventObject evt) throws SipException {
             if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~   " + this + ": "
                     + SipSession.State.toString(mState) + ": processing "
                     + log(evt));
             if (isRequestEvent(Request.INVITE, evt)) {
-                RequestEvent event = (RequestEvent) evt;
-                SipSessionImpl newSession = new SipSessionImpl(mProxy);
-                newSession.mState = SipSession.State.INCOMING_CALL;
-                newSession.mServerTransaction = mSipHelper.sendRinging(event,
-                        generateTag());
-                newSession.mDialog = newSession.mServerTransaction.getDialog();
-                newSession.mInviteReceived = event;
-                newSession.mPeerProfile = createPeerProfile(event.getRequest());
-                newSession.mPeerSessionDescription =
-                        extractContent(event.getRequest());
-                addSipSession(newSession);
-                mProxy.onRinging(newSession, newSession.mPeerProfile,
-                        newSession.mPeerSessionDescription);
+                processNewInviteRequest((RequestEvent) evt);
                 return true;
             } else if (isRequestEvent(Request.OPTIONS, evt)) {
                 mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
diff --git a/voip/java/com/android/server/sip/SipSessionListenerProxy.java b/voip/java/com/android/server/sip/SipSessionListenerProxy.java
index f8be0a8..8655a3a 100644
--- a/voip/java/com/android/server/sip/SipSessionListenerProxy.java
+++ b/voip/java/com/android/server/sip/SipSessionListenerProxy.java
@@ -110,6 +110,20 @@
         });
     }
 
+    public void onCallTransferring(final ISipSession newSession,
+            final String sessionDescription) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onCallTransferring(newSession, sessionDescription);
+                } catch (Throwable t) {
+                    handle(t, "onCallTransferring()");
+                }
+            }
+        });
+    }
+
     public void onCallBusy(final ISipSession session) {
         if (mListener == null) return;
         proxy(new Runnable() {