Move the keepalive process to SipSessionImpl and make it reusable.

Reuse the new component in the original keepalive process and the NAT port
mapping timeout measurement process.

This is the foundation for fixing the following bug.

Bug: 3464181

Change-Id: If7e951c000503fa64843942ad062c4d853e20c8d
diff --git a/java/com/android/server/sip/SipHelper.java b/java/com/android/server/sip/SipHelper.java
index 4ee86b6..018e6de 100644
--- a/java/com/android/server/sip/SipHelper.java
+++ b/java/com/android/server/sip/SipHelper.java
@@ -71,6 +71,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;
@@ -177,17 +178,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,23 +252,29 @@
         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 {
         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);
             request.setContent(sessionDescription,
                     mHeaderFactory.createContentTypeHeader(
                             "application", "sdp"));
@@ -419,9 +428,13 @@
     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);
diff --git a/java/com/android/server/sip/SipService.java b/java/com/android/server/sip/SipService.java
index 5ad5d26..802e56d 100644
--- a/java/com/android/server/sip/SipService.java
+++ b/java/com/android/server/sip/SipService.java
@@ -69,10 +69,11 @@
 public final class SipService extends ISipService.Stub {
     static final String TAG = "SipService";
     static final boolean DEBUGV = false;
-    static final boolean 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 Context mContext;
     private String mLocalIp;
@@ -378,7 +379,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);
@@ -389,7 +390,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();
@@ -459,9 +460,17 @@
         }
     }
 
-    private void startPortMappingLifetimeMeasurement(SipSessionGroup group) {
-        mIntervalMeasurementProcess = new IntervalMeasurementProcess(group);
-        mIntervalMeasurementProcess.start();
+    private void startPortMappingLifetimeMeasurement(
+            SipProfile localProfile) {
+        if ((mIntervalMeasurementProcess == null)
+                && (mKeepAliveInterval == -1)
+                && isBehindNAT(mLocalIp)) {
+            Log.d(TAG, "start NAT port mapping timeout measurement on "
+                    + localProfile.getUriString());
+
+            mIntervalMeasurementProcess = new IntervalMeasurementProcess(localProfile);
+            mIntervalMeasurementProcess.start();
+        }
     }
 
     private synchronized void addPendingSession(ISipSession session) {
@@ -500,6 +509,33 @@
         return false;
     }
 
+    private synchronized void onKeepAliveIntervalChanged() {
+        for (SipSessionGroupExt group : mSipGroups.values()) {
+            group.onKeepAliveIntervalChanged();
+        }
+    }
+
+    private int getKeepAliveInterval() {
+        return (mKeepAliveInterval < 0)
+                ? DEFAULT_KEEPALIVE_INTERVAL
+                : 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;
@@ -527,6 +563,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
@@ -534,7 +580,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?");
@@ -697,158 +743,114 @@
         }
     }
 
-    private class IntervalMeasurementProcess extends SipSessionAdapter
-            implements Runnable {
-        private static final String TAG = "\\INTERVAL/";
+    private class IntervalMeasurementProcess implements
+            SipSessionGroup.KeepAliveProcessCallback {
+        private static final String TAG = "SipKeepAliveInterval";
         private static final int MAX_INTERVAL = 120; // seconds
         private static final int MIN_INTERVAL = SHORT_EXPIRY_TIME;
-        private static final int PASS_THRESHOLD = 6;
+        private static final int PASS_THRESHOLD = 10;
         private SipSessionGroupExt mGroup;
         private SipSessionGroup.SipSessionImpl mSession;
         private boolean mRunning;
-        private int mMinInterval = 10;
+        private int mMinInterval = 10; // in seconds
         private int mMaxInterval = MAX_INTERVAL;
         private int mInterval = MAX_INTERVAL / 2;
         private int mPassCounter = 0;
-        private SipWakeupTimer mTimer = new SipWakeupTimer(mContext, mExecutor);
-        // TODO: fix SipWakeupTimer so that we only use one instance of the timer
 
-        public IntervalMeasurementProcess(SipSessionGroup group) {
+        public IntervalMeasurementProcess(SipProfile localProfile) {
             try {
-                mGroup =  new SipSessionGroupExt(
-                        group.getLocalProfile(), null, null);
+                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));
                 mSession = (SipSessionGroup.SipSessionImpl)
-                        mGroup.createSession(this);
+                        mGroup.createSession(null);
             } catch (Exception e) {
                 Log.w(TAG, "start interval measurement error: " + e);
             }
         }
 
         public void start() {
-            if (mRunning) return;
-            mRunning = true;
-            mTimer.set(mInterval * 1000, this);
-            if (DEBUGV) Log.v(TAG, "start interval measurement");
-            run();
+            synchronized (SipService.this) {
+                try {
+                    mSession.startKeepAliveProcess(mInterval, this);
+                } catch (SipException e) {
+                    Log.e(TAG, "start()", e);
+                }
+            }
         }
 
         public void stop() {
-            mRunning = false;
-            mTimer.cancel(this);
+            synchronized (SipService.this) {
+                mSession.stopKeepAliveProcess();
+            }
         }
 
         private void restart() {
-            mTimer.cancel(this);
-            mTimer.set(mInterval * 1000, this);
-        }
-
-        private void calculateNewInterval() {
-            if (!mSession.isReRegisterRequired()) {
-                if (++mPassCounter != PASS_THRESHOLD) return;
-                // update the interval, since the current interval is good to
-                // keep the port mapping.
-                mKeepAliveInterval = mMinInterval = mInterval;
-            } else {
-                // Since the rport is changed, shorten the interval.
-                mSession.clearReRegisterRequired();
-                mMaxInterval = mInterval;
-            }
-            if ((mMaxInterval - mMinInterval) < MIN_INTERVAL) {
-                // update mKeepAliveInterval and stop measurement.
-                stop();
-                mKeepAliveInterval = mMinInterval;
-                if (DEBUGV) Log.v(TAG, "measured interval: " + mKeepAliveInterval);
-            } else {
-                // calculate the new interval and continue.
-                mInterval = (mMaxInterval + mMinInterval) / 2;
-                mPassCounter = 0;
-                if (DEBUGV) {
-                    Log.v(TAG, " current interval: " + mKeepAliveInterval
-                            + "test new interval: " + mInterval);
-                }
-                restart();
-            }
-        }
-
-        public void run() {
             synchronized (SipService.this) {
-                if (!mRunning) return;
                 try {
-                    mSession.sendKeepAlive();
-                    calculateNewInterval();
-                } catch (Throwable t) {
+                    mSession.stopKeepAliveProcess();
+                    mSession.startKeepAliveProcess(mInterval, this);
+                } catch (SipException e) {
+                    Log.e(TAG, "restart()", e);
+                }
+            }
+        }
+
+        // SipSessionGroup.KeepAliveProcessCallback
+        @Override
+        public void onResponse(boolean portChanged) {
+            synchronized (SipService.this) {
+                if (!portChanged) {
+                    if (++mPassCounter != PASS_THRESHOLD) return;
+                    // update the interval, since the current interval is good to
+                    // keep the port mapping.
+                    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 ((mMaxInterval - mMinInterval) < MIN_INTERVAL) {
+                    // update mKeepAliveInterval and stop measurement.
                     stop();
-                    Log.w(TAG, "interval measurement error: " + t);
+                    mKeepAliveInterval = mMinInterval;
+                    if (DEBUG) {
+                        Log.d(TAG, "measured keepalive interval: "
+                                + mKeepAliveInterval);
+                    }
+                } else {
+                    // calculate the new interval and continue.
+                    mInterval = (mMaxInterval + mMinInterval) / 2;
+                    mPassCounter = 0;
+                    if (DEBUG) {
+                        Log.d(TAG, "current interval: " + mKeepAliveInterval
+                                + ", test new interval: " + mInterval);
+                    }
+                    restart();
                 }
             }
         }
-    }
 
-    // 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 SipSessionGroup.SipSessionImpl mSession;
-        private boolean mRunning = false;
-        private int mInterval = INTERVAL;
-
-        public KeepAliveProcess(SipSessionGroup.SipSessionImpl session) {
-            mSession = session;
-        }
-
-        public void start() {
-            if (mRunning) return;
-            mRunning = true;
-            mTimer.set(INTERVAL * 1000, this);
-        }
-
-        private void restart(int duration) {
-            if (DEBUG) Log.d(TAG, "Refresh NAT port mapping " + duration + "s later.");
-            mTimer.cancel(this);
-            mTimer.set(duration * 1000, this);
-        }
-
-        // timeout handler
-        public void run() {
+        // SipSessionGroup.KeepAliveProcessCallback
+        @Override
+        public void onError(int errorCode, String description) {
             synchronized (SipService.this) {
-                if (!mRunning) return;
-
-                if (DEBUGV) Log.v(TAG, "~~~ keepalive: "
-                        + mSession.getLocalProfile().getUriString());
-                SipSessionGroup.SipSessionImpl session = mSession.duplicate();
-                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);
-                    }
-                    if (mKeepAliveInterval > mInterval) {
-                        mInterval = mKeepAliveInterval;
-                        restart(mInterval);
-                    }
-                } catch (Throwable t) {
-                    Log.w(TAG, "keepalive error: " + t);
-                }
+                Log.w(TAG, "interval measurement error: " + description);
             }
         }
-
-        public void stop() {
-            if (DEBUGV && (mSession != null)) Log.v(TAG, "stop keepalive:"
-                    + mSession.getLocalProfile().getUriString());
-            mRunning = false;
-            mSession = null;
-            mTimer.cancel(this);
-        }
     }
 
     private class AutoRegistrationProcess extends SipSessionAdapter
-            implements Runnable {
+            implements Runnable, SipSessionGroup.KeepAliveProcessCallback {
+        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;
@@ -869,27 +871,38 @@
                 // return right away if no active network connection.
                 if (mSession == null) return;
 
-                synchronized (SipService.this) {
-                    if (isBehindNAT(mLocalIp)
-                            && (mIntervalMeasurementProcess == null)
-                            && (mKeepAliveInterval == -1)) {
-                        // Start keep-alive interval measurement, here we allow
-                        // the first profile only as the target service provider
-                        // to measure the life time of NAT port mapping.
-                        startPortMappingLifetimeMeasurement(group);
-                    }
-                }
-
                 // start unregistration to clear up old registration at server
                 // TODO: when rfc5626 is deployed, use reg-id and sip.instance
                 // 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");
             }
         }
 
+        // SipSessionGroup.KeepAliveProcessCallback
+        @Override
+        public void onResponse(boolean portChanged) {
+            synchronized (SipService.this) {
+                // Start keep-alive interval measurement on the first successfully
+                // kept-alive SipSessionGroup
+                startPortMappingLifetimeMeasurement(mSession.getLocalProfile());
+
+                if (!mRunning || !portChanged) return;
+                // 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);
+        }
+
         public void stop() {
             if (!mRunning) return;
             mRunning = false;
@@ -900,15 +913,30 @@
             }
 
             mTimer.cancel(this);
-            if (mKeepAliveProcess != null) {
-                mKeepAliveProcess.stop();
-                mKeepAliveProcess = null;
+            if (mKeepAliveSession != null) {
+                mKeepAliveSession.stopKeepAliveProcess();
+                mKeepAliveSession = null;
             }
 
             mRegistered = false;
             setListener(mProxy.getListener());
         }
 
+        public void onKeepAliveIntervalChanged() {
+            if (mKeepAliveSession != null) {
+                int newInterval = getKeepAliveInterval();
+                if (DEBUGV) {
+                    Log.v(TAG, "restart keepalive w interval=" + newInterval);
+                }
+                mKeepAliveSession.stopKeepAliveProcess();
+                try {
+                    mKeepAliveSession.startKeepAliveProcess(newInterval, this);
+                } catch (SipException e) {
+                    Log.e(TAG, "onKeepAliveIntervalChanged()", e);
+                }
+            }
+        }
+
         public void setListener(ISipSessionListener listener) {
             synchronized (SipService.this) {
                 mProxy.setListener(listener);
@@ -955,13 +983,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);
@@ -969,22 +998,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);
@@ -1030,7 +1043,6 @@
                 mProxy.onRegistrationDone(session, duration);
 
                 if (duration > 0) {
-                    mSession.clearReRegisterRequired();
                     mExpiryTime = SystemClock.elapsedRealtime()
                             + (duration * 1000);
 
@@ -1043,13 +1055,17 @@
                         }
                         restart(duration);
 
-                        if (isBehindNAT(mLocalIp) ||
-                                mSession.getLocalProfile().getSendKeepAlive()) {
-                            if (mKeepAliveProcess == null) {
-                                mKeepAliveProcess =
-                                        new KeepAliveProcess(mSession);
+                        SipProfile localProfile = mSession.getLocalProfile();
+                        if ((mKeepAliveSession == null) && (isBehindNAT(mLocalIp)
+                                || localProfile.getSendKeepAlive())) {
+                            mKeepAliveSession = mSession.duplicate();
+                            Log.d(TAG, "start keepalive");
+                            try {
+                                mKeepAliveSession.startKeepAliveProcess(
+                                        getKeepAliveInterval(), this);
+                            } catch (SipException e) {
+                                Log.e(TAG, "AutoRegistrationProcess", e);
                             }
-                            mKeepAliveProcess.start();
                         }
                     }
                     mMyWakeLock.release(session);
@@ -1103,10 +1119,6 @@
         private void restartLater() {
             mRegistered = false;
             restart(backoffDuration());
-            if (mKeepAliveProcess != null) {
-                mKeepAliveProcess.stop();
-                mKeepAliveProcess = null;
-            }
         }
     }
 
diff --git a/java/com/android/server/sip/SipSessionGroup.java b/java/com/android/server/sip/SipSessionGroup.java
index 4837eb9..cc3e410 100644
--- a/java/com/android/server/sip/SipSessionGroup.java
+++ b/java/com/android/server/sip/SipSessionGroup.java
@@ -28,6 +28,7 @@
 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;
 
@@ -89,6 +90,7 @@
     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 long WAKE_LOCK_HOLDING_TIME = 500; // in milliseconds
 
     private static final EventObject DEREGISTER = new EventObject("Deregister");
@@ -107,6 +109,7 @@
     private SipSessionImpl mCallReceiverSession;
     private String mLocalIp;
 
+    private SipWakeupTimer mWakeupTimer;
     private SipWakeLock mWakeLock;
 
     // call-id-to-SipSession map
@@ -119,13 +122,21 @@
      * @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;
@@ -382,6 +393,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 +409,10 @@
         ClientTransaction mClientTransaction;
         String mPeerSessionDescription;
         boolean mInCall;
-        SessionTimer mTimer;
+        SessionTimer mSessionTimer;
         int mAuthenticationRetryCount;
 
-        // for registration
-        boolean mReRegisterFlag = false;
-        int mRPort = 0;
+        private KeepAliveProcess mKeepAliveProcess;
 
         // lightweight timer
         class SessionTimer {
@@ -512,7 +527,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);
                         }
                     }
@@ -553,34 +570,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)) {
@@ -612,6 +601,11 @@
             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();
@@ -627,9 +621,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;
@@ -754,10 +745,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");
@@ -778,48 +765,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)) {
@@ -1138,15 +1083,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;
             }
         }
 
@@ -1272,6 +1217,168 @@
             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;
+
+            // @param interval in seconds
+            void start(int interval, KeepAliveProcessCallback callback) {
+                if (mRunning) return;
+                mRunning = true;
+                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) {
+                                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) {
+                        Log.d(TAG, "keepalive: " + mLocalProfile.getUriString()
+                                + " --> " + mPeerProfile);
+                    }
+                    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();
+            }
+        }
     }
 
     /**
@@ -1363,15 +1470,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;
@@ -1387,12 +1495,6 @@
         }
     }
 
-    private class OptionsCommand extends EventObject {
-        public OptionsCommand() {
-            super(SipSessionGroup.this);
-        }
-    }
-
     private class RegisterCommand extends EventObject {
         private int mDuration;
 
@@ -1434,4 +1536,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);
+                    }
+                }
+            });
+        }
+    }
 }