am 0793586b: am f8c1f129: am e1d27154: am f87743e7: Merge "Prevent NullPointerException cases while using SipService."

* commit '0793586bf8f4dce71d0b4d7ff2f212129b3f76fe':
  Prevent NullPointerException cases while using SipService.
diff --git a/java/android/net/sip/ISipSessionListener.aidl b/java/android/net/sip/ISipSessionListener.aidl
index 5920bca..690700c 100644
--- a/java/android/net/sip/ISipSessionListener.aidl
+++ b/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/java/android/net/sip/SipAudioCall.java b/java/android/net/sip/SipAudioCall.java
index b46f826..fcdbd2c 100644
--- a/java/android/net/sip/SipAudioCall.java
+++ b/java/android/net/sip/SipAudioCall.java
@@ -26,6 +26,7 @@
 import android.net.wifi.WifiManager;
 import android.os.Message;
 import android.os.RemoteException;
+import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.IOException;
@@ -56,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").
@@ -170,6 +172,7 @@
     private SipProfile mLocalProfile;
     private SipAudioCall.Listener mListener;
     private SipSession mSipSession;
+    private SipSession mTransferringSession;
 
     private long mSessionId = System.currentTimeMillis();
     private String mPeerSd;
@@ -347,6 +350,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
@@ -378,6 +402,7 @@
             @Override
             public void onRinging(SipSession session,
                     SipProfile peerProfile, String sessionDescription) {
+                // this callback is triggered only for reinvite.
                 synchronized (SipAudioCall.this) {
                     if ((mSipSession == null) || !mInCall
                             || !session.getCallId().equals(
@@ -404,6 +429,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 +452,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 +531,26 @@
             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;
+                try {
+                    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();
+                }
+            }
         };
     }
 
@@ -675,6 +737,7 @@
     }
 
     private SimpleSessionDescription createAnswer(String offerSd) {
+        if (TextUtils.isEmpty(offerSd)) return createOffer();
         SimpleSessionDescription offer =
                 new SimpleSessionDescription(offerSd);
         SimpleSessionDescription answer =
diff --git a/java/android/net/sip/SipSession.java b/java/android/net/sip/SipSession.java
index 5629b3c..5ba1626 100644
--- a/java/android/net/sip/SipSession.java
+++ b/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/java/android/net/sip/SipSessionAdapter.java b/java/android/net/sip/SipSessionAdapter.java
index 86aca37..f538983 100644
--- a/java/android/net/sip/SipSessionAdapter.java
+++ b/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/java/com/android/server/sip/SipHelper.java b/java/com/android/server/sip/SipHelper.java
index 4ee86b6..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;
@@ -71,6 +74,7 @@
 class SipHelper {
     private static final String TAG = SipHelper.class.getSimpleName();
     private static final boolean DEBUG = true;
+    private static final boolean DEBUG_PING = false;
 
     private SipStack mSipStack;
     private SipProvider mSipProvider;
@@ -149,9 +153,17 @@
 
     private ContactHeader createContactHeader(SipProfile profile)
             throws ParseException, SipException {
-        ListeningPoint lp = getListeningPoint();
-        SipURI contactURI =
-                createSipUri(profile.getUserName(), profile.getProtocol(), lp);
+        return createContactHeader(profile, null, 0);
+    }
+
+    private ContactHeader createContactHeader(SipProfile profile,
+            String ip, int port) throws ParseException,
+            SipException {
+        SipURI contactURI = (ip == null)
+                ? createSipUri(profile.getUserName(), profile.getProtocol(),
+                        getListeningPoint())
+                : createSipUri(profile.getUserName(), profile.getProtocol(),
+                        ip, port);
 
         Address contactAddress = mAddressFactory.createAddress(contactURI);
         contactAddress.setDisplayName(profile.getDisplayName());
@@ -167,9 +179,14 @@
 
     private SipURI createSipUri(String username, String transport,
             ListeningPoint lp) throws ParseException {
-        SipURI uri = mAddressFactory.createSipURI(username, lp.getIPAddress());
+        return createSipUri(username, transport, lp.getIPAddress(), lp.getPort());
+    }
+
+    private SipURI createSipUri(String username, String transport,
+            String ip, int port) throws ParseException {
+        SipURI uri = mAddressFactory.createSipURI(username, ip);
         try {
-            uri.setPort(lp.getPort());
+            uri.setPort(port);
             uri.setTransportParam(transport);
         } catch (InvalidArgumentException e) {
             throw new RuntimeException(e);
@@ -177,17 +194,19 @@
         return uri;
     }
 
-    public ClientTransaction sendKeepAlive(SipProfile userProfile, String tag)
-            throws SipException {
+    public ClientTransaction sendOptions(SipProfile caller, SipProfile callee,
+            String tag) throws SipException {
         try {
-            Request request = createRequest(Request.OPTIONS, userProfile, tag);
+            Request request = (caller == callee)
+                    ? createRequest(Request.OPTIONS, caller, tag)
+                    : createRequest(Request.OPTIONS, caller, callee, tag);
 
             ClientTransaction clientTransaction =
                     mSipProvider.getNewClientTransaction(request);
             clientTransaction.sendRequest();
             return clientTransaction;
         } catch (Exception e) {
-            throw new SipException("sendKeepAlive()", e);
+            throw new SipException("sendOptions()", e);
         }
     }
 
@@ -249,27 +268,37 @@
         return ct;
     }
 
+    private Request createRequest(String requestType, SipProfile caller,
+            SipProfile callee, String tag) throws ParseException, SipException {
+        FromHeader fromHeader = createFromHeader(caller, tag);
+        ToHeader toHeader = createToHeader(callee);
+        SipURI requestURI = callee.getUri();
+        List<ViaHeader> viaHeaders = createViaHeaders();
+        CallIdHeader callIdHeader = createCallIdHeader();
+        CSeqHeader cSeqHeader = createCSeqHeader(requestType);
+        MaxForwardsHeader maxForwards = createMaxForwardsHeader();
+
+        Request request = mMessageFactory.createRequest(requestURI,
+                requestType, callIdHeader, cSeqHeader, fromHeader,
+                toHeader, viaHeaders, maxForwards);
+
+        request.addHeader(createContactHeader(caller));
+        return request;
+    }
+
     public ClientTransaction sendInvite(SipProfile caller, SipProfile callee,
-            String sessionDescription, String tag)
-            throws SipException {
+            String sessionDescription, String tag, ReferredByHeader referredBy,
+            String replaces) throws SipException {
         try {
-            FromHeader fromHeader = createFromHeader(caller, tag);
-            ToHeader toHeader = createToHeader(callee);
-            SipURI requestURI = callee.getUri();
-            List<ViaHeader> viaHeaders = createViaHeaders();
-            CallIdHeader callIdHeader = createCallIdHeader();
-            CSeqHeader cSeqHeader = createCSeqHeader(Request.INVITE);
-            MaxForwardsHeader maxForwards = createMaxForwardsHeader();
-
-            Request request = mMessageFactory.createRequest(requestURI,
-                    Request.INVITE, callIdHeader, cSeqHeader, fromHeader,
-                    toHeader, viaHeaders, maxForwards);
-
-            request.addHeader(createContactHeader(caller));
+            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);
@@ -305,7 +334,7 @@
         }
     }
 
-    private ServerTransaction getServerTransaction(RequestEvent event)
+    public ServerTransaction getServerTransaction(RequestEvent event)
             throws SipException {
         ServerTransaction transaction = event.getServerTransaction();
         if (transaction == null) {
@@ -344,13 +373,14 @@
      */
     public ServerTransaction sendInviteOk(RequestEvent event,
             SipProfile localProfile, String sessionDescription,
-            ServerTransaction inviteTransaction)
-            throws SipException {
+            ServerTransaction inviteTransaction, String externalIp,
+            int externalPort) throws SipException {
         try {
             Request request = event.getRequest();
             Response response = mMessageFactory.createResponse(Response.OK,
                     request);
-            response.addHeader(createContactHeader(localProfile));
+            response.addHeader(createContactHeader(localProfile, externalIp,
+                    externalPort));
             response.setContent(sessionDescription,
                     mHeaderFactory.createContentTypeHeader(
                             "application", "sdp"));
@@ -419,15 +449,38 @@
     public void sendResponse(RequestEvent event, int responseCode)
             throws SipException {
         try {
+            Request request = event.getRequest();
             Response response = mMessageFactory.createResponse(
-                    responseCode, event.getRequest());
-            if (DEBUG) Log.d(TAG, "send response: " + response);
+                    responseCode, request);
+            if (DEBUG && (!Request.OPTIONS.equals(request.getMethod())
+                    || DEBUG_PING)) {
+                Log.d(TAG, "send response: " + response);
+            }
             getServerTransaction(event).sendResponse(response);
         } catch (ParseException e) {
             throw new SipException("sendResponse()", e);
         }
     }
 
+    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/SipService.java b/java/com/android/server/sip/SipService.java
index dc66989..c553947 100644
--- a/java/com/android/server/sip/SipService.java
+++ b/java/com/android/server/sip/SipService.java
@@ -60,6 +60,7 @@
 import java.util.Timer;
 import java.util.TimerTask;
 import java.util.TreeSet;
+import java.util.concurrent.Executor;
 import javax.sip.SipException;
 
 /**
@@ -68,22 +69,26 @@
 public final class SipService extends ISipService.Stub {
     static final String TAG = "SipService";
     static final boolean DEBUGV = false;
-    private static final boolean DEBUG = false;
-    private static final boolean DEBUG_TIMER = DEBUG && false;
+    static final boolean DEBUG = true;
     private static final int EXPIRY_TIME = 3600;
     private static final int SHORT_EXPIRY_TIME = 10;
     private static final int MIN_EXPIRY_TIME = 60;
+    private static final int DEFAULT_KEEPALIVE_INTERVAL = 10; // in seconds
+    private static final int DEFAULT_MAX_KEEPALIVE_INTERVAL = 120; // in seconds
 
     private Context mContext;
     private String mLocalIp;
     private String mNetworkType;
     private boolean mConnected;
-    private WakeupTimer mTimer;
+    private SipWakeupTimer mTimer;
     private WifiScanProcess mWifiScanProcess;
     private WifiManager.WifiLock mWifiLock;
     private boolean mWifiOnly;
+    private BroadcastReceiver mWifiStateReceiver = null;
 
-    private MyExecutor mExecutor;
+    private IntervalMeasurementProcess mIntervalMeasurementProcess;
+
+    private MyExecutor mExecutor = new MyExecutor();
 
     // SipProfile URI --> group
     private Map<String, SipSessionGroupExt> mSipGroups =
@@ -96,6 +101,8 @@
     private ConnectivityReceiver mConnectivityReceiver;
     private boolean mWifiEnabled;
     private SipWakeLock mMyWakeLock;
+    private int mKeepAliveInterval;
+    private int mLastGoodKeepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL;
 
     /**
      * Starts the SIP service. Do nothing if the SIP API is not supported on the
@@ -116,53 +123,54 @@
         mMyWakeLock = new SipWakeLock((PowerManager)
                 context.getSystemService(Context.POWER_SERVICE));
 
-        mTimer = new WakeupTimer(context);
+        mTimer = new SipWakeupTimer(context, mExecutor);
         mWifiOnly = SipManager.isSipWifiOnly(context);
     }
 
-    private BroadcastReceiver mWifiStateReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
-                int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
-                        WifiManager.WIFI_STATE_UNKNOWN);
-                synchronized (SipService.this) {
-                    switch (state) {
-                        case WifiManager.WIFI_STATE_ENABLED:
-                            mWifiEnabled = true;
-                            if (anyOpenedToReceiveCalls()) grabWifiLock();
-                            break;
-                        case WifiManager.WIFI_STATE_DISABLED:
-                            mWifiEnabled = false;
-                            releaseWifiLock();
-                            break;
+    private BroadcastReceiver createWifiBroadcastReceiver() {
+        return new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                String action = intent.getAction();
+                if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
+                    int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
+                            WifiManager.WIFI_STATE_UNKNOWN);
+                    synchronized (SipService.this) {
+                        switch (state) {
+                            case WifiManager.WIFI_STATE_ENABLED:
+                                mWifiEnabled = true;
+                                if (anyOpenedToReceiveCalls()) grabWifiLock();
+                                break;
+                            case WifiManager.WIFI_STATE_DISABLED:
+                                mWifiEnabled = false;
+                                releaseWifiLock();
+                                break;
+                        }
                     }
                 }
             }
-        }
+        };
     };
 
     private void registerReceivers() {
         mContext.registerReceiver(mConnectivityReceiver,
                 new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
-        mContext.registerReceiver(mWifiStateReceiver,
-                new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION));
+        if (SipManager.isSipWifiOnly(mContext)) {
+            mWifiStateReceiver = createWifiBroadcastReceiver();
+            mContext.registerReceiver(mWifiStateReceiver,
+                    new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION));
+        }
         if (DEBUG) Log.d(TAG, " +++ register receivers");
     }
 
     private void unregisterReceivers() {
         mContext.unregisterReceiver(mConnectivityReceiver);
-        mContext.unregisterReceiver(mWifiStateReceiver);
+        if (SipManager.isSipWifiOnly(mContext)) {
+            mContext.unregisterReceiver(mWifiStateReceiver);
+        }
         if (DEBUG) Log.d(TAG, " --- unregister receivers");
     }
 
-    private MyExecutor getExecutor() {
-        // create mExecutor lazily
-        if (mExecutor == null) mExecutor = new MyExecutor();
-        return mExecutor;
-    }
-
     public synchronized SipProfile[] getListOfProfiles() {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.USE_SIP, null);
@@ -382,7 +390,7 @@
 
     private void grabWifiLock() {
         if (mWifiLock == null) {
-            if (DEBUG) Log.d(TAG, "~~~~~~~~~~~~~~~~~~~~~ acquire wifi lock");
+            if (DEBUG) Log.d(TAG, "acquire wifi lock");
             mWifiLock = ((WifiManager)
                     mContext.getSystemService(Context.WIFI_SERVICE))
                     .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
@@ -393,7 +401,7 @@
 
     private void releaseWifiLock() {
         if (mWifiLock != null) {
-            if (DEBUG) Log.d(TAG, "~~~~~~~~~~~~~~~~~~~~~ release wifi lock");
+            if (DEBUG) Log.d(TAG, "release wifi lock");
             mWifiLock.release();
             mWifiLock = null;
             stopWifiScanner();
@@ -441,12 +449,15 @@
 
             if (connected) {
                 mLocalIp = determineLocalIp();
+                mKeepAliveInterval = -1;
+                mLastGoodKeepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL;
                 for (SipSessionGroupExt group : mSipGroups.values()) {
                     group.onConnectivityChanged(true);
                 }
                 if (isWifi && (mWifiLock != null)) stopWifiScanner();
             } else {
                 mMyWakeLock.reset(); // in case there's a leak
+                stopPortMappingMeasurement();
                 if (isWifi && (mWifiLock != null)) startWifiScanner();
             }
         } catch (SipException e) {
@@ -454,6 +465,48 @@
         }
     }
 
+    private void stopPortMappingMeasurement() {
+        if (mIntervalMeasurementProcess != null) {
+            mIntervalMeasurementProcess.stop();
+            mIntervalMeasurementProcess = null;
+        }
+    }
+
+    private void startPortMappingLifetimeMeasurement(
+            SipProfile localProfile) {
+        startPortMappingLifetimeMeasurement(localProfile,
+                DEFAULT_MAX_KEEPALIVE_INTERVAL);
+    }
+
+    private void startPortMappingLifetimeMeasurement(
+            SipProfile localProfile, int maxInterval) {
+        if ((mIntervalMeasurementProcess == null)
+                && (mKeepAliveInterval == -1)
+                && isBehindNAT(mLocalIp)) {
+            Log.d(TAG, "start NAT port mapping timeout measurement on "
+                    + localProfile.getUriString());
+
+            int minInterval = mLastGoodKeepAliveInterval;
+            if (minInterval >= maxInterval) {
+                // If mLastGoodKeepAliveInterval also does not work, reset it
+                // to the default min
+                minInterval = mLastGoodKeepAliveInterval
+                        = DEFAULT_KEEPALIVE_INTERVAL;
+                Log.d(TAG, "  reset min interval to " + minInterval);
+            }
+            mIntervalMeasurementProcess = new IntervalMeasurementProcess(
+                    localProfile, minInterval, maxInterval);
+            mIntervalMeasurementProcess.start();
+        }
+    }
+
+    private void restartPortMappingLifetimeMeasurement(
+            SipProfile localProfile, int maxInterval) {
+        stopPortMappingMeasurement();
+        mKeepAliveInterval = -1;
+        startPortMappingLifetimeMeasurement(localProfile, maxInterval);
+    }
+
     private synchronized void addPendingSession(ISipSession session) {
         try {
             cleanUpPendingSessions();
@@ -490,6 +543,33 @@
         return false;
     }
 
+    private synchronized void onKeepAliveIntervalChanged() {
+        for (SipSessionGroupExt group : mSipGroups.values()) {
+            group.onKeepAliveIntervalChanged();
+        }
+    }
+
+    private int getKeepAliveInterval() {
+        return (mKeepAliveInterval < 0)
+                ? mLastGoodKeepAliveInterval
+                : mKeepAliveInterval;
+    }
+
+    private boolean isBehindNAT(String address) {
+        try {
+            byte[] d = InetAddress.getByName(address).getAddress();
+            if ((d[0] == 10) ||
+                    (((0x000000FF & ((int)d[0])) == 172) &&
+                    ((0x000000F0 & ((int)d[1])) == 16)) ||
+                    (((0x000000FF & ((int)d[0])) == 192) &&
+                    ((0x000000FF & ((int)d[1])) == 168))) {
+                return true;
+            }
+        } catch (UnknownHostException e) {
+            Log.e(TAG, "isBehindAT()" + address, e);
+        }
+        return false;
+    }
 
     private class SipSessionGroupExt extends SipSessionAdapter {
         private SipSessionGroup mSipGroup;
@@ -517,6 +597,16 @@
             return mSipGroup.containsSession(callId);
         }
 
+        public void onKeepAliveIntervalChanged() {
+            mAutoRegistration.onKeepAliveIntervalChanged();
+        }
+
+        // TODO: remove this method once SipWakeupTimer can better handle variety
+        // of timeout values
+        void setWakeupTimer(SipWakeupTimer timer) {
+            mSipGroup.setWakeupTimer(timer);
+        }
+
         // network connectivity is tricky because network can be disconnected
         // at any instant so need to deal with exceptions carefully even when
         // you think you are connected
@@ -524,7 +614,7 @@
                 SipProfile localProfile, String password) throws SipException {
             try {
                 return new SipSessionGroup(localIp, localProfile, password,
-                        mMyWakeLock);
+                        mTimer, mMyWakeLock);
             } catch (IOException e) {
                 // network disconnected
                 Log.w(TAG, "createSipSessionGroup(): network disconnected?");
@@ -687,60 +777,170 @@
         }
     }
 
-    // KeepAliveProcess is controlled by AutoRegistrationProcess.
-    // All methods will be invoked in sync with SipService.this.
-    private class KeepAliveProcess implements Runnable {
-        private static final String TAG = "\\KEEPALIVE/";
-        private static final int INTERVAL = 10;
+    private class IntervalMeasurementProcess implements Runnable,
+            SipSessionGroup.KeepAliveProcessCallback {
+        private static final String TAG = "SipKeepAliveInterval";
+        private static final int MIN_INTERVAL = 5; // in seconds
+        private static final int PASS_THRESHOLD = 10;
+        private static final int MAX_RETRY_COUNT = 5;
+        private static final int NAT_MEASUREMENT_RETRY_INTERVAL = 120; // in seconds
+        private SipSessionGroupExt mGroup;
         private SipSessionGroup.SipSessionImpl mSession;
-        private boolean mRunning = false;
+        private int mMinInterval;
+        private int mMaxInterval;
+        private int mInterval;
+        private int mPassCount = 0;
 
-        public KeepAliveProcess(SipSessionGroup.SipSessionImpl session) {
-            mSession = session;
+        public IntervalMeasurementProcess(SipProfile localProfile,
+                int minInterval, int maxInterval) {
+            mMaxInterval = maxInterval;
+            mMinInterval = minInterval;
+            mInterval = (maxInterval + minInterval) / 2;
+
+            // Don't start measurement if the interval is too small
+            if (mInterval < DEFAULT_KEEPALIVE_INTERVAL) {
+                Log.w(TAG, "interval is too small; measurement aborted; "
+                        + "maxInterval=" + mMaxInterval);
+                return;
+            } else if (checkTermination()) {
+                Log.w(TAG, "interval is too small; measurement aborted; "
+                        + "interval=[" + mMinInterval + "," + mMaxInterval
+                        + "]");
+                return;
+            }
+
+            try {
+                mGroup =  new SipSessionGroupExt(localProfile, null, null);
+                // TODO: remove this line once SipWakeupTimer can better handle
+                // variety of timeout values
+                mGroup.setWakeupTimer(new SipWakeupTimer(mContext, mExecutor));
+            } catch (Exception e) {
+                Log.w(TAG, "start interval measurement error: " + e);
+            }
         }
 
         public void start() {
-            if (mRunning) return;
-            mRunning = true;
-            mTimer.set(INTERVAL * 1000, this);
-        }
-
-        // timeout handler
-        public void run() {
             synchronized (SipService.this) {
-                if (!mRunning) return;
-
-                if (DEBUGV) Log.v(TAG, "~~~ keepalive: "
-                        + mSession.getLocalProfile().getUriString());
-                SipSessionGroup.SipSessionImpl session = mSession.duplicate();
+                Log.d(TAG, "start measurement w interval=" + mInterval);
+                if (mSession == null) {
+                    mSession = (SipSessionGroup.SipSessionImpl)
+                            mGroup.createSession(null);
+                }
                 try {
-                    session.sendKeepAlive();
-                    if (session.isReRegisterRequired()) {
-                        // Acquire wake lock for the registration process. The
-                        // lock will be released when registration is complete.
-                        mMyWakeLock.acquire(mSession);
-                        mSession.register(EXPIRY_TIME);
-                    }
-                } catch (Throwable t) {
-                    Log.w(TAG, "keepalive error: " + t);
+                    mSession.startKeepAliveProcess(mInterval, this);
+                } catch (SipException e) {
+                    Log.e(TAG, "start()", e);
                 }
             }
         }
 
         public void stop() {
-            if (DEBUGV && (mSession != null)) Log.v(TAG, "stop keepalive:"
-                    + mSession.getLocalProfile().getUriString());
-            mRunning = false;
-            mSession = null;
+            synchronized (SipService.this) {
+                if (mSession != null) {
+                    mSession.stopKeepAliveProcess();
+                    mSession = null;
+                }
+                mTimer.cancel(this);
+            }
+        }
+
+        private void restart() {
+            synchronized (SipService.this) {
+                // Return immediately if the measurement process is stopped
+                if (mSession == null) return;
+
+                Log.d(TAG, "restart measurement w interval=" + mInterval);
+                try {
+                    mSession.stopKeepAliveProcess();
+                    mPassCount = 0;
+                    mSession.startKeepAliveProcess(mInterval, this);
+                } catch (SipException e) {
+                    Log.e(TAG, "restart()", e);
+                }
+            }
+        }
+
+        private boolean checkTermination() {
+            return ((mMaxInterval - mMinInterval) < MIN_INTERVAL);
+        }
+
+        // SipSessionGroup.KeepAliveProcessCallback
+        @Override
+        public void onResponse(boolean portChanged) {
+            synchronized (SipService.this) {
+                if (!portChanged) {
+                    if (++mPassCount != PASS_THRESHOLD) return;
+                    // update the interval, since the current interval is good to
+                    // keep the port mapping.
+                    if (mKeepAliveInterval > 0) {
+                        mLastGoodKeepAliveInterval = mKeepAliveInterval;
+                    }
+                    mKeepAliveInterval = mMinInterval = mInterval;
+                    if (DEBUG) {
+                        Log.d(TAG, "measured good keepalive interval: "
+                                + mKeepAliveInterval);
+                    }
+                    onKeepAliveIntervalChanged();
+                } else {
+                    // Since the rport is changed, shorten the interval.
+                    mMaxInterval = mInterval;
+                }
+                if (checkTermination()) {
+                    // update mKeepAliveInterval and stop measurement.
+                    stop();
+                    // If all the measurements failed, we still set it to
+                    // mMinInterval; If mMinInterval still doesn't work, a new
+                    // measurement with min interval=DEFAULT_KEEPALIVE_INTERVAL
+                    // will be conducted.
+                    mKeepAliveInterval = mMinInterval;
+                    if (DEBUG) {
+                        Log.d(TAG, "measured keepalive interval: "
+                                + mKeepAliveInterval);
+                    }
+                } else {
+                    // calculate the new interval and continue.
+                    mInterval = (mMaxInterval + mMinInterval) / 2;
+                    if (DEBUG) {
+                        Log.d(TAG, "current interval: " + mKeepAliveInterval
+                                + ", test new interval: " + mInterval);
+                    }
+                    restart();
+                }
+            }
+        }
+
+        // SipSessionGroup.KeepAliveProcessCallback
+        @Override
+        public void onError(int errorCode, String description) {
+            Log.w(TAG, "interval measurement error: " + description);
+            restartLater();
+        }
+
+        // timeout handler
+        @Override
+        public void run() {
             mTimer.cancel(this);
+            restart();
+        }
+
+        private void restartLater() {
+            synchronized (SipService.this) {
+                int interval = NAT_MEASUREMENT_RETRY_INTERVAL;
+                Log.d(TAG, "Retry measurement " + interval + "s later.");
+                mTimer.cancel(this);
+                mTimer.set(interval * 1000, this);
+            }
         }
     }
 
     private class AutoRegistrationProcess extends SipSessionAdapter
-            implements Runnable {
+            implements Runnable, SipSessionGroup.KeepAliveProcessCallback {
+        private static final int MIN_KEEPALIVE_SUCCESS_COUNT = 10;
+        private String TAG = "SipAudoReg";
+
         private SipSessionGroup.SipSessionImpl mSession;
+        private SipSessionGroup.SipSessionImpl mKeepAliveSession;
         private SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
-        private KeepAliveProcess mKeepAliveProcess;
         private int mBackoff = 1;
         private boolean mRegistered;
         private long mExpiryTime;
@@ -748,6 +948,8 @@
         private String mErrorMessage;
         private boolean mRunning = false;
 
+        private int mKeepAliveSuccessCount = 0;
+
         private String getAction() {
             return toString();
         }
@@ -766,11 +968,84 @@
                 // in registration to avoid adding duplicate entries to server
                 mMyWakeLock.acquire(mSession);
                 mSession.unregister();
-                if (DEBUG) Log.d(TAG, "start AutoRegistrationProcess for "
-                        + mSession.getLocalProfile().getUriString());
+                if (DEBUG) TAG = mSession.getLocalProfile().getUriString();
+                if (DEBUG) Log.d(TAG, "start AutoRegistrationProcess");
             }
         }
 
+        private void startKeepAliveProcess(int interval) {
+            Log.d(TAG, "start keepalive w interval=" + interval);
+            if (mKeepAliveSession == null) {
+                mKeepAliveSession = mSession.duplicate();
+            } else {
+                mKeepAliveSession.stopKeepAliveProcess();
+            }
+            try {
+                mKeepAliveSession.startKeepAliveProcess(interval, this);
+            } catch (SipException e) {
+                Log.e(TAG, "failed to start keepalive w interval=" + interval,
+                        e);
+            }
+        }
+
+        private void stopKeepAliveProcess() {
+            if (mKeepAliveSession != null) {
+                mKeepAliveSession.stopKeepAliveProcess();
+                mKeepAliveSession = null;
+            }
+            mKeepAliveSuccessCount = 0;
+        }
+
+        // SipSessionGroup.KeepAliveProcessCallback
+        @Override
+        public void onResponse(boolean portChanged) {
+            synchronized (SipService.this) {
+                if (portChanged) {
+                    int interval = getKeepAliveInterval();
+                    if (mKeepAliveSuccessCount < MIN_KEEPALIVE_SUCCESS_COUNT) {
+                        Log.i(TAG, "keepalive doesn't work with interval "
+                                + interval + ", past success count="
+                                + mKeepAliveSuccessCount);
+                        if (interval > DEFAULT_KEEPALIVE_INTERVAL) {
+                            restartPortMappingLifetimeMeasurement(
+                                    mSession.getLocalProfile(), interval);
+                            mKeepAliveSuccessCount = 0;
+                        }
+                    } else {
+                        Log.i(TAG, "keep keepalive going with interval "
+                                + interval + ", past success count="
+                                + mKeepAliveSuccessCount);
+                        mKeepAliveSuccessCount /= 2;
+                    }
+                } else {
+                    // Start keep-alive interval measurement on the first
+                    // successfully kept-alive SipSessionGroup
+                    startPortMappingLifetimeMeasurement(
+                            mSession.getLocalProfile());
+                    mKeepAliveSuccessCount++;
+                }
+
+                if (!mRunning || !portChanged) return;
+
+                // The keep alive process is stopped when port is changed;
+                // Nullify the session so that the process can be restarted
+                // again when the re-registration is done
+                mKeepAliveSession = null;
+
+                // Acquire wake lock for the registration process. The
+                // lock will be released when registration is complete.
+                mMyWakeLock.acquire(mSession);
+                mSession.register(EXPIRY_TIME);
+            }
+        }
+
+        // SipSessionGroup.KeepAliveProcessCallback
+        @Override
+        public void onError(int errorCode, String description) {
+            Log.e(TAG, "keepalive error: " + description);
+            onResponse(true); // re-register immediately
+        }
+
         public void stop() {
             if (!mRunning) return;
             mRunning = false;
@@ -781,15 +1056,23 @@
             }
 
             mTimer.cancel(this);
-            if (mKeepAliveProcess != null) {
-                mKeepAliveProcess.stop();
-                mKeepAliveProcess = null;
-            }
+            stopKeepAliveProcess();
 
             mRegistered = false;
             setListener(mProxy.getListener());
         }
 
+        public void onKeepAliveIntervalChanged() {
+            if (mKeepAliveSession != null) {
+                int newInterval = getKeepAliveInterval();
+                if (DEBUGV) {
+                    Log.v(TAG, "restart keepalive w interval=" + newInterval);
+                }
+                mKeepAliveSuccessCount = 0;
+                startKeepAliveProcess(newInterval);
+            }
+        }
+
         public void setListener(ISipSessionListener listener) {
             synchronized (SipService.this) {
                 mProxy.setListener(listener);
@@ -836,13 +1119,14 @@
         }
 
         // timeout handler: re-register
+        @Override
         public void run() {
             synchronized (SipService.this) {
                 if (!mRunning) return;
 
                 mErrorCode = SipErrorCode.NO_ERROR;
                 mErrorMessage = null;
-                if (DEBUG) Log.d(TAG, "~~~ registering");
+                if (DEBUG) Log.d(TAG, "registering");
                 if (mConnected) {
                     mMyWakeLock.acquire(mSession);
                     mSession.register(EXPIRY_TIME);
@@ -850,22 +1134,6 @@
             }
         }
 
-        private boolean isBehindNAT(String address) {
-            try {
-                byte[] d = InetAddress.getByName(address).getAddress();
-                if ((d[0] == 10) ||
-                        (((0x000000FF & ((int)d[0])) == 172) &&
-                        ((0x000000F0 & ((int)d[1])) == 16)) ||
-                        (((0x000000FF & ((int)d[0])) == 192) &&
-                        ((0x000000FF & ((int)d[1])) == 168))) {
-                    return true;
-                }
-            } catch (UnknownHostException e) {
-                Log.e(TAG, "isBehindAT()" + address, e);
-            }
-            return false;
-        }
-
         private void restart(int duration) {
             if (DEBUG) Log.d(TAG, "Refresh registration " + duration + "s later.");
             mTimer.cancel(this);
@@ -911,7 +1179,6 @@
                 mProxy.onRegistrationDone(session, duration);
 
                 if (duration > 0) {
-                    mSession.clearReRegisterRequired();
                     mExpiryTime = SystemClock.elapsedRealtime()
                             + (duration * 1000);
 
@@ -924,13 +1191,10 @@
                         }
                         restart(duration);
 
-                        if (isBehindNAT(mLocalIp) ||
-                                mSession.getLocalProfile().getSendKeepAlive()) {
-                            if (mKeepAliveProcess == null) {
-                                mKeepAliveProcess =
-                                        new KeepAliveProcess(mSession);
-                            }
-                            mKeepAliveProcess.start();
+                        SipProfile localProfile = mSession.getLocalProfile();
+                        if ((mKeepAliveSession == null) && (isBehindNAT(mLocalIp)
+                                || localProfile.getSendKeepAlive())) {
+                            startKeepAliveProcess(getKeepAliveInterval());
                         }
                     }
                     mMyWakeLock.release(session);
@@ -984,10 +1248,6 @@
         private void restartLater() {
             mRegistered = false;
             restart(backoffDuration());
-            if (mKeepAliveProcess != null) {
-                mKeepAliveProcess.stop();
-                mKeepAliveProcess = null;
-            }
         }
     }
 
@@ -998,7 +1258,7 @@
         @Override
         public void onReceive(final Context context, final Intent intent) {
             // Run the handler in MyExecutor to be protected by wake lock
-            getExecutor().execute(new Runnable() {
+            mExecutor.execute(new Runnable() {
                 public void run() {
                     onReceiveInternal(context, intent);
                 }
@@ -1102,7 +1362,7 @@
             @Override
             public void run() {
                 // delegate to mExecutor
-                getExecutor().execute(new Runnable() {
+                mExecutor.execute(new Runnable() {
                     public void run() {
                         realRun();
                     }
@@ -1127,300 +1387,6 @@
         }
     }
 
-    /**
-     * Timer that can schedule events to occur even when the device is in sleep.
-     * Only used internally in this package.
-     */
-    class WakeupTimer extends BroadcastReceiver {
-        private static final String TAG = "_SIP.WkTimer_";
-        private static final String TRIGGER_TIME = "TriggerTime";
-
-        private Context mContext;
-        private AlarmManager mAlarmManager;
-
-        // runnable --> time to execute in SystemClock
-        private TreeSet<MyEvent> mEventQueue =
-                new TreeSet<MyEvent>(new MyEventComparator());
-
-        private PendingIntent mPendingIntent;
-
-        public WakeupTimer(Context context) {
-            mContext = context;
-            mAlarmManager = (AlarmManager)
-                    context.getSystemService(Context.ALARM_SERVICE);
-
-            IntentFilter filter = new IntentFilter(getAction());
-            context.registerReceiver(this, filter);
-        }
-
-        /**
-         * Stops the timer. No event can be scheduled after this method is called.
-         */
-        public synchronized void stop() {
-            mContext.unregisterReceiver(this);
-            if (mPendingIntent != null) {
-                mAlarmManager.cancel(mPendingIntent);
-                mPendingIntent = null;
-            }
-            mEventQueue.clear();
-            mEventQueue = null;
-        }
-
-        private synchronized boolean stopped() {
-            if (mEventQueue == null) {
-                Log.w(TAG, "Timer stopped");
-                return true;
-            } else {
-                return false;
-            }
-        }
-
-        private void cancelAlarm() {
-            mAlarmManager.cancel(mPendingIntent);
-            mPendingIntent = null;
-        }
-
-        private void recalculatePeriods() {
-            if (mEventQueue.isEmpty()) return;
-
-            MyEvent firstEvent = mEventQueue.first();
-            int minPeriod = firstEvent.mMaxPeriod;
-            long minTriggerTime = firstEvent.mTriggerTime;
-            for (MyEvent e : mEventQueue) {
-                e.mPeriod = e.mMaxPeriod / minPeriod * minPeriod;
-                int interval = (int) (e.mLastTriggerTime + e.mMaxPeriod
-                        - minTriggerTime);
-                interval = interval / minPeriod * minPeriod;
-                e.mTriggerTime = minTriggerTime + interval;
-            }
-            TreeSet<MyEvent> newQueue = new TreeSet<MyEvent>(
-                    mEventQueue.comparator());
-            newQueue.addAll((Collection<MyEvent>) mEventQueue);
-            mEventQueue.clear();
-            mEventQueue = newQueue;
-            if (DEBUG_TIMER) {
-                Log.d(TAG, "queue re-calculated");
-                printQueue();
-            }
-        }
-
-        // Determines the period and the trigger time of the new event and insert it
-        // to the queue.
-        private void insertEvent(MyEvent event) {
-            long now = SystemClock.elapsedRealtime();
-            if (mEventQueue.isEmpty()) {
-                event.mTriggerTime = now + event.mPeriod;
-                mEventQueue.add(event);
-                return;
-            }
-            MyEvent firstEvent = mEventQueue.first();
-            int minPeriod = firstEvent.mPeriod;
-            if (minPeriod <= event.mMaxPeriod) {
-                event.mPeriod = event.mMaxPeriod / minPeriod * minPeriod;
-                int interval = event.mMaxPeriod;
-                interval -= (int) (firstEvent.mTriggerTime - now);
-                interval = interval / minPeriod * minPeriod;
-                event.mTriggerTime = firstEvent.mTriggerTime + interval;
-                mEventQueue.add(event);
-            } else {
-                long triggerTime = now + event.mPeriod;
-                if (firstEvent.mTriggerTime < triggerTime) {
-                    event.mTriggerTime = firstEvent.mTriggerTime;
-                    event.mLastTriggerTime -= event.mPeriod;
-                } else {
-                    event.mTriggerTime = triggerTime;
-                }
-                mEventQueue.add(event);
-                recalculatePeriods();
-            }
-        }
-
-        /**
-         * Sets a periodic timer.
-         *
-         * @param period the timer period; in milli-second
-         * @param callback is called back when the timer goes off; the same callback
-         *      can be specified in multiple timer events
-         */
-        public synchronized void set(int period, Runnable callback) {
-            if (stopped()) return;
-
-            long now = SystemClock.elapsedRealtime();
-            MyEvent event = new MyEvent(period, callback, now);
-            insertEvent(event);
-
-            if (mEventQueue.first() == event) {
-                if (mEventQueue.size() > 1) cancelAlarm();
-                scheduleNext();
-            }
-
-            long triggerTime = event.mTriggerTime;
-            if (DEBUG_TIMER) {
-                Log.d(TAG, " add event " + event + " scheduled at "
-                        + showTime(triggerTime) + " at " + showTime(now)
-                        + ", #events=" + mEventQueue.size());
-                printQueue();
-            }
-        }
-
-        /**
-         * Cancels all the timer events with the specified callback.
-         *
-         * @param callback the callback
-         */
-        public synchronized void cancel(Runnable callback) {
-            if (stopped() || mEventQueue.isEmpty()) return;
-            if (DEBUG_TIMER) Log.d(TAG, "cancel:" + callback);
-
-            MyEvent firstEvent = mEventQueue.first();
-            for (Iterator<MyEvent> iter = mEventQueue.iterator();
-                    iter.hasNext();) {
-                MyEvent event = iter.next();
-                if (event.mCallback == callback) {
-                    iter.remove();
-                    if (DEBUG_TIMER) Log.d(TAG, "    cancel found:" + event);
-                }
-            }
-            if (mEventQueue.isEmpty()) {
-                cancelAlarm();
-            } else if (mEventQueue.first() != firstEvent) {
-                cancelAlarm();
-                firstEvent = mEventQueue.first();
-                firstEvent.mPeriod = firstEvent.mMaxPeriod;
-                firstEvent.mTriggerTime = firstEvent.mLastTriggerTime
-                        + firstEvent.mPeriod;
-                recalculatePeriods();
-                scheduleNext();
-            }
-            if (DEBUG_TIMER) {
-                Log.d(TAG, "after cancel:");
-                printQueue();
-            }
-        }
-
-        private void scheduleNext() {
-            if (stopped() || mEventQueue.isEmpty()) return;
-
-            if (mPendingIntent != null) {
-                throw new RuntimeException("pendingIntent is not null!");
-            }
-
-            MyEvent event = mEventQueue.first();
-            Intent intent = new Intent(getAction());
-            intent.putExtra(TRIGGER_TIME, event.mTriggerTime);
-            PendingIntent pendingIntent = mPendingIntent =
-                    PendingIntent.getBroadcast(mContext, 0, intent,
-                            PendingIntent.FLAG_UPDATE_CURRENT);
-            mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
-                    event.mTriggerTime, pendingIntent);
-        }
-
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            // This callback is already protected by AlarmManager's wake lock.
-            String action = intent.getAction();
-            if (getAction().equals(action)
-                    && intent.getExtras().containsKey(TRIGGER_TIME)) {
-                mPendingIntent = null;
-                long triggerTime = intent.getLongExtra(TRIGGER_TIME, -1L);
-                execute(triggerTime);
-            } else {
-                Log.d(TAG, "unrecognized intent: " + intent);
-            }
-        }
-
-        private void printQueue() {
-            int count = 0;
-            for (MyEvent event : mEventQueue) {
-                Log.d(TAG, "     " + event + ": scheduled at "
-                        + showTime(event.mTriggerTime) + ": last at "
-                        + showTime(event.mLastTriggerTime));
-                if (++count >= 5) break;
-            }
-            if (mEventQueue.size() > count) {
-                Log.d(TAG, "     .....");
-            } else if (count == 0) {
-                Log.d(TAG, "     <empty>");
-            }
-        }
-
-        private synchronized void execute(long triggerTime) {
-            if (DEBUG_TIMER) Log.d(TAG, "time's up, triggerTime = "
-                    + showTime(triggerTime) + ": " + mEventQueue.size());
-            if (stopped() || mEventQueue.isEmpty()) return;
-
-            for (MyEvent event : mEventQueue) {
-                if (event.mTriggerTime != triggerTime) break;
-                if (DEBUG_TIMER) Log.d(TAG, "execute " + event);
-
-                event.mLastTriggerTime = event.mTriggerTime;
-                event.mTriggerTime += event.mPeriod;
-
-                // run the callback in the handler thread to prevent deadlock
-                getExecutor().execute(event.mCallback);
-            }
-            if (DEBUG_TIMER) {
-                Log.d(TAG, "after timeout execution");
-                printQueue();
-            }
-            scheduleNext();
-        }
-
-        private String getAction() {
-            return toString();
-        }
-
-        private String showTime(long time) {
-            int ms = (int) (time % 1000);
-            int s = (int) (time / 1000);
-            int m = s / 60;
-            s %= 60;
-            return String.format("%d.%d.%d", m, s, ms);
-        }
-    }
-
-    private static class MyEvent {
-        int mPeriod;
-        int mMaxPeriod;
-        long mTriggerTime;
-        long mLastTriggerTime;
-        Runnable mCallback;
-
-        MyEvent(int period, Runnable callback, long now) {
-            mPeriod = mMaxPeriod = period;
-            mCallback = callback;
-            mLastTriggerTime = now;
-        }
-
-        @Override
-        public String toString() {
-            String s = super.toString();
-            s = s.substring(s.indexOf("@"));
-            return s + ":" + (mPeriod / 1000) + ":" + (mMaxPeriod / 1000) + ":"
-                    + toString(mCallback);
-        }
-
-        private String toString(Object o) {
-            String s = o.toString();
-            int index = s.indexOf("$");
-            if (index > 0) s = s.substring(index + 1);
-            return s;
-        }
-    }
-
-    private static class MyEventComparator implements Comparator<MyEvent> {
-        public int compare(MyEvent e1, MyEvent e2) {
-            if (e1 == e2) return 0;
-            int diff = e1.mMaxPeriod - e2.mMaxPeriod;
-            if (diff == 0) diff = -1;
-            return diff;
-        }
-
-        public boolean equals(Object that) {
-            return (this == that);
-        }
-    }
-
     private static Looper createLooper() {
         HandlerThread thread = new HandlerThread("SipService.Executor");
         thread.start();
@@ -1429,12 +1395,13 @@
 
     // Executes immediate tasks in a single thread.
     // Hold/release wake lock for running tasks
-    private class MyExecutor extends Handler {
+    private class MyExecutor extends Handler implements Executor {
         MyExecutor() {
             super(createLooper());
         }
 
-        void execute(Runnable task) {
+        @Override
+        public void execute(Runnable task) {
             mMyWakeLock.acquire(task);
             Message.obtain(this, 0/* don't care */, task).sendToTarget();
         }
diff --git a/java/com/android/server/sip/SipSessionGroup.java b/java/com/android/server/sip/SipSessionGroup.java
index aa616a9..48d9b17 100644
--- a/java/com/android/server/sip/SipSessionGroup.java
+++ b/java/com/android/server/sip/SipSessionGroup.java
@@ -18,16 +18,22 @@
 
 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;
 import android.net.sip.SipErrorCode;
 import android.net.sip.SipProfile;
 import android.net.sip.SipSession;
+import android.net.sip.SipSessionAdapter;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -68,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.
  */
@@ -89,6 +98,8 @@
     private static final String THREAD_POOL_SIZE = "1";
     private static final int EXPIRY_TIME = 3600; // in seconds
     private static final int CANCEL_CALL_TIMER = 3; // in seconds
+    private static final int KEEPALIVE_TIMEOUT = 3; // in seconds
+    private static final int INCALL_KEEPALIVE_INTERVAL = 10; // in seconds
     private static final long WAKE_LOCK_HOLDING_TIME = 500; // in milliseconds
 
     private static final EventObject DEREGISTER = new EventObject("Deregister");
@@ -107,25 +118,38 @@
     private SipSessionImpl mCallReceiverSession;
     private String mLocalIp;
 
+    private SipWakeupTimer mWakeupTimer;
     private SipWakeLock mWakeLock;
 
     // call-id-to-SipSession map
     private Map<String, SipSessionImpl> mSessionMap =
             new HashMap<String, SipSessionImpl>();
 
+    // external address observed from any response
+    private String mExternalIp;
+    private int mExternalPort;
+
     /**
      * @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,
-            SipWakeLock wakeLock) throws SipException, IOException {
+            SipWakeupTimer timer, SipWakeLock wakeLock) throws SipException,
+            IOException {
         mLocalProfile = myself;
         mPassword = password;
+        mWakeupTimer = timer;
         mWakeLock = wakeLock;
         reset(localIp);
     }
 
+    // TODO: remove this method once SipWakeupTimer can better handle variety
+    // of timeout values
+    void setWakeupTimer(SipWakeupTimer timer) {
+        mWakeupTimer = timer;
+    }
+
     synchronized void reset(String localIp) throws SipException, IOException {
         mLocalIp = localIp;
         if (localIp == null) return;
@@ -161,6 +185,8 @@
 
         mCallReceiverSession = null;
         mSessionMap.clear();
+
+        resetExternalAddress();
     }
 
     synchronized void onConnectivityChanged() {
@@ -176,6 +202,12 @@
         }
     }
 
+    synchronized void resetExternalAddress() {
+        Log.d(TAG, " reset external addr on " + mSipStack);
+        mExternalIp = null;
+        mExternalPort = 0;
+    }
+
     public SipProfile getLocalProfile() {
         return mLocalProfile;
     }
@@ -349,29 +381,108 @@
         return null;
     }
 
+    private void extractExternalAddress(ResponseEvent evt) {
+        Response response = evt.getResponse();
+        ViaHeader viaHeader = (ViaHeader)(response.getHeader(
+                SIPHeaderNames.VIA));
+        if (viaHeader == null) return;
+        int rport = viaHeader.getRPort();
+        String externalIp = viaHeader.getReceived();
+        if ((rport > 0) && (externalIp != null)) {
+            mExternalIp = externalIp;
+            mExternalPort = rport;
+            Log.d(TAG, " got external addr " + externalIp + ":" + rport
+                    + " on " + mSipStack);
+        }
+    }
+
+    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 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),
+                            SipSession.State.INCOMING_CALL);
+                    newSession.mProxy.onCallTransferring(newSession,
+                            newSession.mPeerSessionDescription);
+                } else {
+                    mSipHelper.sendResponse(event, response);
+                }
+            } else {
+                // New Incoming call.
+                newSession = createNewSession(event, mProxy,
+                        mSipHelper.sendRinging(event, generateTag()),
+                        SipSession.State.INCOMING_CALL);
+                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);
@@ -382,6 +493,12 @@
         }
     }
 
+    static interface KeepAliveProcessCallback {
+        /** Invoked when the response of keeping alive comes back. */
+        void onResponse(boolean portChanged);
+        void onError(int errorCode, String description);
+    }
+
     class SipSessionImpl extends ISipSession.Stub {
         SipProfile mPeerProfile;
         SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
@@ -392,12 +509,17 @@
         ClientTransaction mClientTransaction;
         String mPeerSessionDescription;
         boolean mInCall;
-        SessionTimer mTimer;
+        SessionTimer mSessionTimer;
         int mAuthenticationRetryCount;
 
-        // for registration
-        boolean mReRegisterFlag = false;
-        int mRPort;
+        private KeepAliveProcess mKeepAliveProcess;
+
+        private SipSessionImpl mKeepAliveSession;
+
+        // the following three members are used for handling refer request.
+        SipSessionImpl mReferSession;
+        ReferredByHeader mReferredBy;
+        String mReplaces;
 
         // lightweight timer
         class SessionTimer {
@@ -447,8 +569,10 @@
             mState = SipSession.State.READY_TO_CALL;
             mInviteReceived = null;
             mPeerSessionDescription = null;
-            mRPort = 0;
             mAuthenticationRetryCount = 0;
+            mReferSession = null;
+            mReferredBy = null;
+            mReplaces = null;
 
             if (mDialog != null) mDialog.delete();
             mDialog = null;
@@ -468,6 +592,11 @@
             mClientTransaction = null;
 
             cancelSessionTimer();
+
+            if (mKeepAliveSession != null) {
+                mKeepAliveSession.stopKeepAliveProcess();
+                mKeepAliveSession = null;
+            }
         }
 
         public boolean isInCall() {
@@ -513,7 +642,9 @@
                         try {
                             processCommand(command);
                         } catch (Throwable e) {
-                            Log.w(TAG, "command error: " + command, e);
+                            Log.w(TAG, "command error: " + command + ": "
+                                    + mLocalProfile.getUriString(),
+                                    getRootCause(e));
                             onError(e);
                         }
                     }
@@ -529,12 +660,8 @@
         public void answerCall(String sessionDescription, int timeout) {
             synchronized (SipSessionGroup.this) {
                 if (mPeerProfile == null) return;
-                try {
-                    processCommand(new MakeCallCommand(mPeerProfile,
-                            sessionDescription, timeout));
-                } catch (SipException e) {
-                    onError(e);
-                }
+                doCommandAsync(new MakeCallCommand(mPeerProfile,
+                        sessionDescription, timeout));
             }
         }
 
@@ -558,34 +685,6 @@
             doCommandAsync(DEREGISTER);
         }
 
-        public boolean isReRegisterRequired() {
-            return mReRegisterFlag;
-        }
-
-        public void clearReRegisterRequired() {
-            mReRegisterFlag = false;
-        }
-
-        public void sendKeepAlive() {
-            mState = SipSession.State.PINGING;
-            try {
-                processCommand(new OptionsCommand());
-                for (int i = 0; i < 15; i++) {
-                    if (SipSession.State.PINGING != mState) break;
-                    Thread.sleep(200);
-                }
-                if (SipSession.State.PINGING == mState) {
-                    // FIXME: what to do if server doesn't respond
-                    reset();
-                    if (DEBUG) Log.w(TAG, "no response from ping");
-                }
-            } 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 (isLoggable(command)) Log.d(TAG, "process cmd: " + command);
             if (!process(command)) {
@@ -617,11 +716,17 @@
             synchronized (SipSessionGroup.this) {
                 if (isClosed()) return false;
 
+                if (mKeepAliveProcess != null) {
+                    // event consumed by keepalive process
+                    if (mKeepAliveProcess.process(evt)) return true;
+                }
+
                 Dialog dialog = null;
                 if (evt instanceof RequestEvent) {
                     dialog = ((RequestEvent) evt).getDialog();
                 } else if (evt instanceof ResponseEvent) {
                     dialog = ((ResponseEvent) evt).getDialog();
+                    extractExternalAddress((ResponseEvent) evt);
                 }
                 if (dialog != null) mDialog = dialog;
 
@@ -632,9 +737,6 @@
                 case SipSession.State.DEREGISTERING:
                     processed = registeringToReady(evt);
                     break;
-                case SipSession.State.PINGING:
-                    processed = keepAliveProcess(evt);
-                    break;
                 case SipSession.State.READY_TO_CALL:
                     processed = readyForCall(evt);
                     break;
@@ -759,10 +861,6 @@
                 case SipSession.State.OUTGOING_CALL_CANCELING:
                     onError(SipErrorCode.TIME_OUT, event.toString());
                     break;
-                case SipSession.State.PINGING:
-                    reset();
-                    mReRegisterFlag = true;
-                    break;
 
                 default:
                     Log.d(TAG, "   do nothing");
@@ -783,48 +881,6 @@
             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;
-                        if (DEBUG) Log.w(TAG, String.format(
-                                "rport is changed: %d <> %d", mRPort, rPort));
-                        mRPort = rPort;
-                    } else {
-                        if (DEBUG_PING) Log.w(TAG, "rport is the same: " + rPort);
-                    }
-                } else {
-                    if (DEBUG) Log.w(TAG, "peer did not respond rport");
-                }
-                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)) {
@@ -930,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());
@@ -973,7 +1040,8 @@
                 mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived,
                         mLocalProfile,
                         ((MakeCallCommand) evt).getSessionDescription(),
-                        mServerTransaction);
+                        mServerTransaction,
+                        mExternalIp, mExternalPort);
                 startSessionTimer(((MakeCallCommand) evt).getTimeout());
                 return true;
             } else if (END_CALL == evt) {
@@ -996,7 +1064,13 @@
                 throws SipException {
             // expect ACK, CANCEL request
             if (isRequestEvent(Request.ACK, evt)) {
-                establishCall();
+                String sdp = extractContent(((RequestEvent) evt).getRequest());
+                if (sdp != null) mPeerSessionDescription = sdp;
+                if (mPeerSessionDescription == null) {
+                    onError(SipErrorCode.CLIENT_ERROR, "peer sdp is empty");
+                } else {
+                    establishCall(false);
+                }
                 return true;
             } else if (isRequestEvent(Request.CANCEL, evt)) {
                 // http://tools.ietf.org/html/rfc3261#section-9.2
@@ -1026,9 +1100,15 @@
                     }
                     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();
+                    establishCall(true);
                     return true;
                 case Response.UNAUTHORIZED:
                 case Response.PROXY_AUTHENTICATION_REQUIRED:
@@ -1041,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);
@@ -1109,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
@@ -1129,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;
@@ -1136,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;
         }
@@ -1143,15 +1263,15 @@
         // timeout in seconds
         private void startSessionTimer(int timeout) {
             if (timeout > 0) {
-                mTimer = new SessionTimer();
-                mTimer.start(timeout);
+                mSessionTimer = new SessionTimer();
+                mSessionTimer.start(timeout);
             }
         }
 
         private void cancelSessionTimer() {
-            if (mTimer != null) {
-                mTimer.cancel();
-                mTimer = null;
+            if (mSessionTimer != null) {
+                mSessionTimer.cancel();
+                mSessionTimer = null;
             }
         }
 
@@ -1160,10 +1280,26 @@
                     response.getStatusCode());
         }
 
-        private void establishCall() {
+        private void enableKeepAlive() {
+            if (mKeepAliveSession != null) {
+                mKeepAliveSession.stopKeepAliveProcess();
+            } else {
+                mKeepAliveSession = duplicate();
+            }
+            try {
+                mKeepAliveSession.startKeepAliveProcess(
+                        INCALL_KEEPALIVE_INTERVAL, mPeerProfile, null);
+            } catch (SipException e) {
+                Log.w(TAG, "keepalive cannot be enabled; ignored", e);
+                mKeepAliveSession.stopKeepAliveProcess();
+            }
+        }
+
+        private void establishCall(boolean enableKeepAlive) {
             mState = SipSession.State.IN_CALL;
-            mInCall = true;
             cancelSessionTimer();
+            if (!mInCall && enableKeepAlive) enableKeepAlive();
+            mInCall = true;
             mProxy.onCallEstablished(this, mPeerSessionDescription);
         }
 
@@ -1277,6 +1413,174 @@
             onRegistrationFailed(getErrorCode(statusCode),
                     createErrorMessage(response));
         }
+
+        // Notes: SipSessionListener will be replaced by the keepalive process
+        // @param interval in seconds
+        public void startKeepAliveProcess(int interval,
+                KeepAliveProcessCallback callback) throws SipException {
+            synchronized (SipSessionGroup.this) {
+                startKeepAliveProcess(interval, mLocalProfile, callback);
+            }
+        }
+
+        // Notes: SipSessionListener will be replaced by the keepalive process
+        // @param interval in seconds
+        public void startKeepAliveProcess(int interval, SipProfile peerProfile,
+                KeepAliveProcessCallback callback) throws SipException {
+            synchronized (SipSessionGroup.this) {
+                if (mKeepAliveProcess != null) {
+                    throw new SipException("Cannot create more than one "
+                            + "keepalive process in a SipSession");
+                }
+                mPeerProfile = peerProfile;
+                mKeepAliveProcess = new KeepAliveProcess();
+                mProxy.setListener(mKeepAliveProcess);
+                mKeepAliveProcess.start(interval, callback);
+            }
+        }
+
+        public void stopKeepAliveProcess() {
+            synchronized (SipSessionGroup.this) {
+                if (mKeepAliveProcess != null) {
+                    mKeepAliveProcess.stop();
+                    mKeepAliveProcess = null;
+                }
+            }
+        }
+
+        class KeepAliveProcess extends SipSessionAdapter implements Runnable {
+            private static final String TAG = "SipKeepAlive";
+            private boolean mRunning = false;
+            private KeepAliveProcessCallback mCallback;
+
+            private boolean mPortChanged = false;
+            private int mRPort = 0;
+            private int mInterval; // just for debugging
+
+            // @param interval in seconds
+            void start(int interval, KeepAliveProcessCallback callback) {
+                if (mRunning) return;
+                mRunning = true;
+                mInterval = interval;
+                mCallback = new KeepAliveProcessCallbackProxy(callback);
+                mWakeupTimer.set(interval * 1000, this);
+                if (DEBUG) {
+                    Log.d(TAG, "start keepalive:"
+                            + mLocalProfile.getUriString());
+                }
+
+                // No need to run the first time in a separate thread for now
+                run();
+            }
+
+            // return true if the event is consumed
+            boolean process(EventObject evt) throws SipException {
+                if (mRunning && (mState == SipSession.State.PINGING)) {
+                    if (evt instanceof ResponseEvent) {
+                        if (parseOptionsResult(evt)) {
+                            if (mPortChanged) {
+                                resetExternalAddress();
+                                stop();
+                            } else {
+                                cancelSessionTimer();
+                                removeSipSession(SipSessionImpl.this);
+                            }
+                            mCallback.onResponse(mPortChanged);
+                            return true;
+                        }
+                    }
+                }
+                return false;
+            }
+
+            // SipSessionAdapter
+            // To react to the session timeout event and network error.
+            @Override
+            public void onError(ISipSession session, int errorCode, String message) {
+                stop();
+                mCallback.onError(errorCode, message);
+            }
+
+            // SipWakeupTimer timeout handler
+            // To send out keepalive message.
+            @Override
+            public void run() {
+                synchronized (SipSessionGroup.this) {
+                    if (!mRunning) return;
+
+                    if (DEBUG_PING) {
+                        String peerUri = (mPeerProfile == null)
+                                ? "null"
+                                : mPeerProfile.getUriString();
+                        Log.d(TAG, "keepalive: " + mLocalProfile.getUriString()
+                                + " --> " + peerUri + ", interval=" + mInterval);
+                    }
+                    try {
+                        sendKeepAlive();
+                    } catch (Throwable t) {
+                        Log.w(TAG, "keepalive error: " + ": "
+                                + mLocalProfile.getUriString(), getRootCause(t));
+                        // It's possible that the keepalive process is being stopped
+                        // during session.sendKeepAlive() so need to check mRunning
+                        // again here.
+                        if (mRunning) SipSessionImpl.this.onError(t);
+                    }
+                }
+            }
+
+            void stop() {
+                synchronized (SipSessionGroup.this) {
+                    if (DEBUG) {
+                        Log.d(TAG, "stop keepalive:" + mLocalProfile.getUriString()
+                                + ",RPort=" + mRPort);
+                    }
+                    mRunning = false;
+                    mWakeupTimer.cancel(this);
+                    reset();
+                }
+            }
+
+            private void sendKeepAlive() throws SipException, InterruptedException {
+                synchronized (SipSessionGroup.this) {
+                    mState = SipSession.State.PINGING;
+                    mClientTransaction = mSipHelper.sendOptions(
+                            mLocalProfile, mPeerProfile, generateTag());
+                    mDialog = mClientTransaction.getDialog();
+                    addSipSession(SipSessionImpl.this);
+
+                    startSessionTimer(KEEPALIVE_TIMEOUT);
+                    // when timed out, onError() will be called with SipErrorCode.TIME_OUT
+                }
+            }
+
+            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) {
+                            mPortChanged = true;
+                            if (DEBUG) Log.d(TAG, String.format(
+                                    "rport is changed: %d <> %d", mRPort, rPort));
+                            mRPort = rPort;
+                        } else {
+                            if (DEBUG) Log.d(TAG, "rport is the same: " + rPort);
+                        }
+                    } else {
+                        if (DEBUG) Log.w(TAG, "peer did not respond rport");
+                    }
+                    return true;
+                }
+                return false;
+            }
+
+            private int getRPortFromResponse(Response response) {
+                ViaHeader viaHeader = (ViaHeader)(response.getHeader(
+                        SIPHeaderNames.VIA));
+                return (viaHeader == null) ? -1 : viaHeader.getRPort();
+            }
+        }
     }
 
     /**
@@ -1328,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;
@@ -1368,15 +1670,16 @@
         if (!isLoggable(s)) return false;
         if (evt == null) return false;
 
-        if (evt instanceof OptionsCommand) {
-            return DEBUG_PING;
-        } else if (evt instanceof ResponseEvent) {
+        if (evt instanceof ResponseEvent) {
             Response response = ((ResponseEvent) evt).getResponse();
             if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) {
                 return DEBUG_PING;
             }
             return DEBUG;
         } else if (evt instanceof RequestEvent) {
+            if (isRequestEvent(Request.OPTIONS, evt)) {
+                return DEBUG_PING;
+            }
             return DEBUG;
         }
         return false;
@@ -1392,12 +1695,6 @@
         }
     }
 
-    private class OptionsCommand extends EventObject {
-        public OptionsCommand() {
-            super(SipSessionGroup.this);
-        }
-    }
-
     private class RegisterCommand extends EventObject {
         private int mDuration;
 
@@ -1439,4 +1736,46 @@
             return mTimeout;
         }
     }
+
+    /** Class to help safely run KeepAliveProcessCallback in a different thread. */
+    static class KeepAliveProcessCallbackProxy implements KeepAliveProcessCallback {
+        private KeepAliveProcessCallback mCallback;
+
+        KeepAliveProcessCallbackProxy(KeepAliveProcessCallback callback) {
+            mCallback = callback;
+        }
+
+        private void proxy(Runnable runnable) {
+            // One thread for each calling back.
+            // Note: Guarantee ordering if the issue becomes important. Currently,
+            // the chance of handling two callback events at a time is none.
+            new Thread(runnable, "SIP-KeepAliveProcessCallbackThread").start();
+        }
+
+        public void onResponse(final boolean portChanged) {
+            if (mCallback == null) return;
+            proxy(new Runnable() {
+                public void run() {
+                    try {
+                        mCallback.onResponse(portChanged);
+                    } catch (Throwable t) {
+                        Log.w(TAG, "onResponse", t);
+                    }
+                }
+            });
+        }
+
+        public void onError(final int errorCode, final String description) {
+            if (mCallback == null) return;
+            proxy(new Runnable() {
+                public void run() {
+                    try {
+                        mCallback.onError(errorCode, description);
+                    } catch (Throwable t) {
+                        Log.w(TAG, "onError", t);
+                    }
+                }
+            });
+        }
+    }
 }
diff --git a/java/com/android/server/sip/SipSessionListenerProxy.java b/java/com/android/server/sip/SipSessionListenerProxy.java
index f8be0a8..8655a3a 100644
--- a/java/com/android/server/sip/SipSessionListenerProxy.java
+++ b/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() {
diff --git a/java/com/android/server/sip/SipWakeupTimer.java b/java/com/android/server/sip/SipWakeupTimer.java
new file mode 100644
index 0000000..00d47ac
--- /dev/null
+++ b/java/com/android/server/sip/SipWakeupTimer.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2011, 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 android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.TreeSet;
+import java.util.concurrent.Executor;
+import javax.sip.SipException;
+
+/**
+ * Timer that can schedule events to occur even when the device is in sleep.
+ */
+class SipWakeupTimer extends BroadcastReceiver {
+    private static final String TAG = "_SIP.WkTimer_";
+    private static final String TRIGGER_TIME = "TriggerTime";
+    private static final boolean DEBUG_TIMER = SipService.DEBUG && false;
+
+    private Context mContext;
+    private AlarmManager mAlarmManager;
+
+    // runnable --> time to execute in SystemClock
+    private TreeSet<MyEvent> mEventQueue =
+            new TreeSet<MyEvent>(new MyEventComparator());
+
+    private PendingIntent mPendingIntent;
+
+    private Executor mExecutor;
+
+    public SipWakeupTimer(Context context, Executor executor) {
+        mContext = context;
+        mAlarmManager = (AlarmManager)
+                context.getSystemService(Context.ALARM_SERVICE);
+
+        IntentFilter filter = new IntentFilter(getAction());
+        context.registerReceiver(this, filter);
+        mExecutor = executor;
+    }
+
+    /**
+     * Stops the timer. No event can be scheduled after this method is called.
+     */
+    public synchronized void stop() {
+        mContext.unregisterReceiver(this);
+        if (mPendingIntent != null) {
+            mAlarmManager.cancel(mPendingIntent);
+            mPendingIntent = null;
+        }
+        mEventQueue.clear();
+        mEventQueue = null;
+    }
+
+    private boolean stopped() {
+        if (mEventQueue == null) {
+            Log.w(TAG, "Timer stopped");
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private void cancelAlarm() {
+        mAlarmManager.cancel(mPendingIntent);
+        mPendingIntent = null;
+    }
+
+    private void recalculatePeriods() {
+        if (mEventQueue.isEmpty()) return;
+
+        MyEvent firstEvent = mEventQueue.first();
+        int minPeriod = firstEvent.mMaxPeriod;
+        long minTriggerTime = firstEvent.mTriggerTime;
+        for (MyEvent e : mEventQueue) {
+            e.mPeriod = e.mMaxPeriod / minPeriod * minPeriod;
+            int interval = (int) (e.mLastTriggerTime + e.mMaxPeriod
+                    - minTriggerTime);
+            interval = interval / minPeriod * minPeriod;
+            e.mTriggerTime = minTriggerTime + interval;
+        }
+        TreeSet<MyEvent> newQueue = new TreeSet<MyEvent>(
+                mEventQueue.comparator());
+        newQueue.addAll((Collection<MyEvent>) mEventQueue);
+        mEventQueue.clear();
+        mEventQueue = newQueue;
+        if (DEBUG_TIMER) {
+            Log.d(TAG, "queue re-calculated");
+            printQueue();
+        }
+    }
+
+    // Determines the period and the trigger time of the new event and insert it
+    // to the queue.
+    private void insertEvent(MyEvent event) {
+        long now = SystemClock.elapsedRealtime();
+        if (mEventQueue.isEmpty()) {
+            event.mTriggerTime = now + event.mPeriod;
+            mEventQueue.add(event);
+            return;
+        }
+        MyEvent firstEvent = mEventQueue.first();
+        int minPeriod = firstEvent.mPeriod;
+        if (minPeriod <= event.mMaxPeriod) {
+            event.mPeriod = event.mMaxPeriod / minPeriod * minPeriod;
+            int interval = event.mMaxPeriod;
+            interval -= (int) (firstEvent.mTriggerTime - now);
+            interval = interval / minPeriod * minPeriod;
+            event.mTriggerTime = firstEvent.mTriggerTime + interval;
+            mEventQueue.add(event);
+        } else {
+            long triggerTime = now + event.mPeriod;
+            if (firstEvent.mTriggerTime < triggerTime) {
+                event.mTriggerTime = firstEvent.mTriggerTime;
+                event.mLastTriggerTime -= event.mPeriod;
+            } else {
+                event.mTriggerTime = triggerTime;
+            }
+            mEventQueue.add(event);
+            recalculatePeriods();
+        }
+    }
+
+    /**
+     * Sets a periodic timer.
+     *
+     * @param period the timer period; in milli-second
+     * @param callback is called back when the timer goes off; the same callback
+     *      can be specified in multiple timer events
+     */
+    public synchronized void set(int period, Runnable callback) {
+        if (stopped()) return;
+
+        long now = SystemClock.elapsedRealtime();
+        MyEvent event = new MyEvent(period, callback, now);
+        insertEvent(event);
+
+        if (mEventQueue.first() == event) {
+            if (mEventQueue.size() > 1) cancelAlarm();
+            scheduleNext();
+        }
+
+        long triggerTime = event.mTriggerTime;
+        if (DEBUG_TIMER) {
+            Log.d(TAG, " add event " + event + " scheduled on "
+                    + showTime(triggerTime) + " at " + showTime(now)
+                    + ", #events=" + mEventQueue.size());
+            printQueue();
+        }
+    }
+
+    /**
+     * Cancels all the timer events with the specified callback.
+     *
+     * @param callback the callback
+     */
+    public synchronized void cancel(Runnable callback) {
+        if (stopped() || mEventQueue.isEmpty()) return;
+        if (DEBUG_TIMER) Log.d(TAG, "cancel:" + callback);
+
+        MyEvent firstEvent = mEventQueue.first();
+        for (Iterator<MyEvent> iter = mEventQueue.iterator();
+                iter.hasNext();) {
+            MyEvent event = iter.next();
+            if (event.mCallback == callback) {
+                iter.remove();
+                if (DEBUG_TIMER) Log.d(TAG, "    cancel found:" + event);
+            }
+        }
+        if (mEventQueue.isEmpty()) {
+            cancelAlarm();
+        } else if (mEventQueue.first() != firstEvent) {
+            cancelAlarm();
+            firstEvent = mEventQueue.first();
+            firstEvent.mPeriod = firstEvent.mMaxPeriod;
+            firstEvent.mTriggerTime = firstEvent.mLastTriggerTime
+                    + firstEvent.mPeriod;
+            recalculatePeriods();
+            scheduleNext();
+        }
+        if (DEBUG_TIMER) {
+            Log.d(TAG, "after cancel:");
+            printQueue();
+        }
+    }
+
+    private void scheduleNext() {
+        if (stopped() || mEventQueue.isEmpty()) return;
+
+        if (mPendingIntent != null) {
+            throw new RuntimeException("pendingIntent is not null!");
+        }
+
+        MyEvent event = mEventQueue.first();
+        Intent intent = new Intent(getAction());
+        intent.putExtra(TRIGGER_TIME, event.mTriggerTime);
+        PendingIntent pendingIntent = mPendingIntent =
+                PendingIntent.getBroadcast(mContext, 0, intent,
+                        PendingIntent.FLAG_UPDATE_CURRENT);
+        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                event.mTriggerTime, pendingIntent);
+    }
+
+    @Override
+    public synchronized void onReceive(Context context, Intent intent) {
+        // This callback is already protected by AlarmManager's wake lock.
+        String action = intent.getAction();
+        if (getAction().equals(action)
+                && intent.getExtras().containsKey(TRIGGER_TIME)) {
+            mPendingIntent = null;
+            long triggerTime = intent.getLongExtra(TRIGGER_TIME, -1L);
+            execute(triggerTime);
+        } else {
+            Log.d(TAG, "unrecognized intent: " + intent);
+        }
+    }
+
+    private void printQueue() {
+        int count = 0;
+        for (MyEvent event : mEventQueue) {
+            Log.d(TAG, "     " + event + ": scheduled at "
+                    + showTime(event.mTriggerTime) + ": last at "
+                    + showTime(event.mLastTriggerTime));
+            if (++count >= 5) break;
+        }
+        if (mEventQueue.size() > count) {
+            Log.d(TAG, "     .....");
+        } else if (count == 0) {
+            Log.d(TAG, "     <empty>");
+        }
+    }
+
+    private void execute(long triggerTime) {
+        if (DEBUG_TIMER) Log.d(TAG, "time's up, triggerTime = "
+                + showTime(triggerTime) + ": " + mEventQueue.size());
+        if (stopped() || mEventQueue.isEmpty()) return;
+
+        for (MyEvent event : mEventQueue) {
+            if (event.mTriggerTime != triggerTime) continue;
+            if (DEBUG_TIMER) Log.d(TAG, "execute " + event);
+
+            event.mLastTriggerTime = triggerTime;
+            event.mTriggerTime += event.mPeriod;
+
+            // run the callback in the handler thread to prevent deadlock
+            mExecutor.execute(event.mCallback);
+        }
+        if (DEBUG_TIMER) {
+            Log.d(TAG, "after timeout execution");
+            printQueue();
+        }
+        scheduleNext();
+    }
+
+    private String getAction() {
+        return toString();
+    }
+
+    private String showTime(long time) {
+        int ms = (int) (time % 1000);
+        int s = (int) (time / 1000);
+        int m = s / 60;
+        s %= 60;
+        return String.format("%d.%d.%d", m, s, ms);
+    }
+
+    private static class MyEvent {
+        int mPeriod;
+        int mMaxPeriod;
+        long mTriggerTime;
+        long mLastTriggerTime;
+        Runnable mCallback;
+
+        MyEvent(int period, Runnable callback, long now) {
+            mPeriod = mMaxPeriod = period;
+            mCallback = callback;
+            mLastTriggerTime = now;
+        }
+
+        @Override
+        public String toString() {
+            String s = super.toString();
+            s = s.substring(s.indexOf("@"));
+            return s + ":" + (mPeriod / 1000) + ":" + (mMaxPeriod / 1000) + ":"
+                    + toString(mCallback);
+        }
+
+        private String toString(Object o) {
+            String s = o.toString();
+            int index = s.indexOf("$");
+            if (index > 0) s = s.substring(index + 1);
+            return s;
+        }
+    }
+
+    // Sort the events by mMaxPeriod so that the first event can be used to
+    // align events with larger periods
+    private static class MyEventComparator implements Comparator<MyEvent> {
+        public int compare(MyEvent e1, MyEvent e2) {
+            if (e1 == e2) return 0;
+            int diff = e1.mMaxPeriod - e2.mMaxPeriod;
+            if (diff == 0) diff = -1;
+            return diff;
+        }
+
+        public boolean equals(Object that) {
+            return (this == that);
+        }
+    }
+}
diff --git a/jni/rtp/Android.mk b/jni/rtp/Android.mk
index 76c43ba..0815294 100644
--- a/jni/rtp/Android.mk
+++ b/jni/rtp/Android.mk
@@ -37,9 +37,9 @@
 	libcutils \
 	libutils \
 	libmedia \
-	libstagefright
+	libstagefright_amrnb_common
 
-LOCAL_STATIC_LIBRARIES := libgsm
+LOCAL_STATIC_LIBRARIES := libgsm libstagefright_amrnbdec libstagefright_amrnbenc
 
 LOCAL_C_INCLUDES += \
 	$(JNI_H_INCLUDE) \
@@ -49,10 +49,11 @@
 	frameworks/base/media/libstagefright/codecs/amrnb/enc/include \
 	frameworks/base/media/libstagefright/codecs/amrnb/enc/src \
 	frameworks/base/media/libstagefright/codecs/amrnb/dec/include \
-	frameworks/base/media/libstagefright/codecs/amrnb/dec/src
+	frameworks/base/media/libstagefright/codecs/amrnb/dec/src \
+	system/media/audio_effects/include
 
 LOCAL_CFLAGS += -fvisibility=hidden
 
-LOCAL_PRELINK_MODULE := false
+
 
 include $(BUILD_SHARED_LIBRARY)
diff --git a/jni/rtp/AudioCodec.cpp b/jni/rtp/AudioCodec.cpp
index 2267ea0..c75fbc9 100644
--- a/jni/rtp/AudioCodec.cpp
+++ b/jni/rtp/AudioCodec.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include <string.h>
+#include <strings.h>
 
 #include "AudioCodec.h"
 
diff --git a/jni/rtp/AudioGroup.cpp b/jni/rtp/AudioGroup.cpp
index 41fedce..529b425 100644
--- a/jni/rtp/AudioGroup.cpp
+++ b/jni/rtp/AudioGroup.cpp
@@ -40,6 +40,9 @@
 #include <media/AudioRecord.h>
 #include <media/AudioTrack.h>
 #include <media/mediarecorder.h>
+#include <media/AudioEffect.h>
+#include <audio_effects/effect_aec.h>
+#include <system/audio.h>
 
 #include "jni.h"
 #include "JNIHelp.h"
@@ -479,6 +482,7 @@
     bool sendDtmf(int event);
     bool add(AudioStream *stream);
     bool remove(int socket);
+    bool platformHasAec() { return mPlatformHasAec; }
 
 private:
     enum {
@@ -489,6 +493,8 @@
         LAST_MODE = 3,
     };
 
+    bool checkPlatformAec();
+
     AudioStream *mChain;
     int mEventQueue;
     volatile int mDtmfEvent;
@@ -497,6 +503,7 @@
     int mSampleRate;
     int mSampleCount;
     int mDeviceSocket;
+    bool mPlatformHasAec;
 
     class NetworkThread : public Thread
     {
@@ -548,6 +555,7 @@
     mDeviceSocket = -1;
     mNetworkThread = new NetworkThread(this);
     mDeviceThread = new DeviceThread(this);
+    mPlatformHasAec = checkPlatformAec();
 }
 
 AudioGroup::~AudioGroup()
@@ -628,10 +636,6 @@
     if (mode == NORMAL && !strcmp(value, "herring")) {
         mode = ECHO_SUPPRESSION;
     }
-    if (mode == ECHO_SUPPRESSION && AudioSystem::getParameters(
-        0, String8("ec_supported")) == "ec_supported=yes") {
-        mode = NORMAL;
-    }
     if (mMode == mode) {
         return true;
     }
@@ -757,6 +761,25 @@
     return true;
 }
 
+bool AudioGroup::checkPlatformAec()
+{
+    effect_descriptor_t fxDesc;
+    uint32_t numFx;
+
+    if (AudioEffect::queryNumberEffects(&numFx) != NO_ERROR) {
+        return false;
+    }
+    for (uint32_t i = 0; i < numFx; i++) {
+        if (AudioEffect::queryEffect(i, &fxDesc) != NO_ERROR) {
+            continue;
+        }
+        if (memcmp(&fxDesc.type, FX_IID_AEC, sizeof(effect_uuid_t)) == 0) {
+            return true;
+        }
+    }
+    return false;
+}
+
 bool AudioGroup::DeviceThread::threadLoop()
 {
     int mode = mGroup->mMode;
@@ -767,10 +790,10 @@
     // Find out the frame count for AudioTrack and AudioRecord.
     int output = 0;
     int input = 0;
-    if (AudioTrack::getMinFrameCount(&output, AudioSystem::VOICE_CALL,
+    if (AudioTrack::getMinFrameCount(&output, AUDIO_STREAM_VOICE_CALL,
         sampleRate) != NO_ERROR || output <= 0 ||
         AudioRecord::getMinFrameCount(&input, sampleRate,
-        AudioSystem::PCM_16_BIT, 1) != NO_ERROR || input <= 0) {
+        AUDIO_FORMAT_PCM_16_BIT, 1) != NO_ERROR || input <= 0) {
         LOGE("cannot compute frame count");
         return false;
     }
@@ -787,19 +810,15 @@
     // Initialize AudioTrack and AudioRecord.
     AudioTrack track;
     AudioRecord record;
-    if (track.set(AudioSystem::VOICE_CALL, sampleRate, AudioSystem::PCM_16_BIT,
-        AudioSystem::CHANNEL_OUT_MONO, output) != NO_ERROR || record.set(
-        AUDIO_SOURCE_VOICE_COMMUNICATION, sampleRate, AudioSystem::PCM_16_BIT,
-        AudioSystem::CHANNEL_IN_MONO, input) != NO_ERROR) {
+    if (track.set(AUDIO_STREAM_VOICE_CALL, sampleRate, AUDIO_FORMAT_PCM_16_BIT,
+        AUDIO_CHANNEL_OUT_MONO, output) != NO_ERROR || record.set(
+        AUDIO_SOURCE_VOICE_COMMUNICATION, sampleRate, AUDIO_FORMAT_PCM_16_BIT,
+        AUDIO_CHANNEL_IN_MONO, input) != NO_ERROR) {
         LOGE("cannot initialize audio device");
         return false;
     }
     LOGD("latency: output %d, input %d", track.latency(), record.latency());
 
-    // Initialize echo canceler.
-    EchoSuppressor echo(sampleCount,
-        (track.latency() + record.latency()) * sampleRate / 1000);
-
     // Give device socket a reasonable buffer size.
     setsockopt(deviceSocket, SOL_SOCKET, SO_RCVBUF, &output, sizeof(output));
     setsockopt(deviceSocket, SOL_SOCKET, SO_SNDBUF, &output, sizeof(output));
@@ -808,6 +827,33 @@
     char c;
     while (recv(deviceSocket, &c, 1, MSG_DONTWAIT) == 1);
 
+    // check if platform supports echo cancellation and do not active local echo suppression in
+    // this case
+    EchoSuppressor *echo = NULL;
+    AudioEffect *aec = NULL;
+    if (mode == ECHO_SUPPRESSION) {
+        if (mGroup->platformHasAec()) {
+            aec = new AudioEffect(FX_IID_AEC,
+                                    NULL,
+                                    0,
+                                    0,
+                                    0,
+                                    record.getSessionId(),
+                                    record.getInput());
+            status_t status = aec->initCheck();
+            if (status == NO_ERROR || status == ALREADY_EXISTS) {
+                aec->setEnabled(true);
+            } else {
+                delete aec;
+                aec = NULL;
+            }
+        }
+        // Create local echo suppressor if platform AEC cannot be used.
+        if (aec == NULL) {
+             echo = new EchoSuppressor(sampleCount,
+                                       (track.latency() + record.latency()) * sampleRate / 1000);
+        }
+    }
     // Start AudioRecord before AudioTrack. This prevents AudioTrack from being
     // disabled due to buffer underrun while waiting for AudioRecord.
     if (mode != MUTED) {
@@ -841,7 +887,7 @@
                     track.releaseBuffer(&buffer);
                 } else if (status != TIMED_OUT && status != WOULD_BLOCK) {
                     LOGE("cannot write to AudioTrack");
-                    return true;
+                    goto exit;
                 }
             }
 
@@ -857,7 +903,7 @@
                     record.releaseBuffer(&buffer);
                 } else if (status != TIMED_OUT && status != WOULD_BLOCK) {
                     LOGE("cannot read from AudioRecord");
-                    return true;
+                    goto exit;
                 }
             }
         }
@@ -868,15 +914,18 @@
         }
 
         if (mode != MUTED) {
-            if (mode == NORMAL) {
-                send(deviceSocket, input, sizeof(input), MSG_DONTWAIT);
-            } else {
-                echo.run(output, input);
-                send(deviceSocket, input, sizeof(input), MSG_DONTWAIT);
+            if (echo != NULL) {
+                LOGV("echo->run()");
+                echo->run(output, input);
             }
+            send(deviceSocket, input, sizeof(input), MSG_DONTWAIT);
         }
     }
-    return false;
+
+exit:
+    delete echo;
+    delete aec;
+    return true;
 }
 
 //------------------------------------------------------------------------------