Add SIP service into system server.

Change-Id: Icc39e4e54768cfdcc1b20a3efe6206009b9a8d10
diff --git a/Android.mk b/Android.mk
index af50134..b6f25047 100644
--- a/Android.mk
+++ b/Android.mk
@@ -182,6 +182,10 @@
 	wifi/java/android/net/wifi/IWifiManager.aidl \
 	telephony/java/com/android/internal/telephony/IExtendedNetworkService.aidl \
 	vpn/java/android/net/vpn/IVpnService.aidl \
+	voip/java/android/net/sip/ISipSession.aidl \
+	voip/java/android/net/sip/ISipSessionListener.aidl \
+	voip/java/android/net/sip/ISipService.aidl
+#
 
 
 # FRAMEWORKS_BASE_JAVA_SRC_DIRS comes from build/core/pathmap.mk
@@ -567,6 +571,7 @@
 # ============================================================
 
 ext_dirs := \
+	../../external/nist-sip/java \
 	../../external/apache-http/src \
 	../../external/tagsoup/src
 
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index aecdcb3..0100550 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -1546,6 +1546,15 @@
     public static final String DOWNLOAD_SERVICE = "download";
 
     /**
+     * Use with {@link #getSystemService} to retrieve a
+     * {@link android.net.sip.SipManager} for accessing the SIP related service.
+     *
+     * @see #getSystemService
+     */
+    /** @hide */
+    public static final String SIP_SERVICE = "sip";
+
+    /**
      * Determine whether the given permission is allowed for a particular
      * process and user ID running in the system.
      *
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index ab869bb..1a209e2 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -17,6 +17,7 @@
 package com.android.server;
 
 import com.android.server.am.ActivityManagerService;
+import com.android.server.sip.SipService;
 import com.android.internal.os.BinderInternal;
 import com.android.internal.os.SamplingProfilerIntegration;
 
@@ -415,6 +416,13 @@
             } catch (Throwable e) {
                 Slog.e(TAG, "Failure starting DiskStats Service", e);
             }
+
+            try {
+                Slog.i(TAG, "Sip Service");
+                ServiceManager.addService("sip", new SipService(context));
+            } catch (Throwable e) {
+                Slog.e(TAG, "Failure starting DiskStats Service", e);
+            }
         }
 
         // make sure the ADB_ENABLED setting value matches the secure property value
diff --git a/services/java/com/android/server/sip/SipHelper.java b/services/java/com/android/server/sip/SipHelper.java
new file mode 100644
index 0000000..83eeb84
--- /dev/null
+++ b/services/java/com/android/server/sip/SipHelper.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.sip;
+
+import gov.nist.javax.sip.SipStackExt;
+import gov.nist.javax.sip.clientauthutils.AccountManager;
+import gov.nist.javax.sip.clientauthutils.AuthenticationHelper;
+
+import android.net.sip.SessionDescription;
+import android.net.sip.SipProfile;
+import android.util.Log;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.EventObject;
+import java.util.List;
+import javax.sip.ClientTransaction;
+import javax.sip.Dialog;
+import javax.sip.DialogTerminatedEvent;
+import javax.sip.InvalidArgumentException;
+import javax.sip.ListeningPoint;
+import javax.sip.PeerUnavailableException;
+import javax.sip.RequestEvent;
+import javax.sip.ResponseEvent;
+import javax.sip.ServerTransaction;
+import javax.sip.SipException;
+import javax.sip.SipFactory;
+import javax.sip.SipProvider;
+import javax.sip.SipStack;
+import javax.sip.Transaction;
+import javax.sip.TransactionAlreadyExistsException;
+import javax.sip.TransactionTerminatedEvent;
+import javax.sip.TransactionUnavailableException;
+import javax.sip.TransactionState;
+import javax.sip.address.Address;
+import javax.sip.address.AddressFactory;
+import javax.sip.address.SipURI;
+import javax.sip.header.CSeqHeader;
+import javax.sip.header.CallIdHeader;
+import javax.sip.header.ContactHeader;
+import javax.sip.header.FromHeader;
+import javax.sip.header.Header;
+import javax.sip.header.HeaderFactory;
+import javax.sip.header.MaxForwardsHeader;
+import javax.sip.header.ToHeader;
+import javax.sip.header.ViaHeader;
+import javax.sip.message.Message;
+import javax.sip.message.MessageFactory;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+
+/**
+ * Helper class for holding SIP stack related classes and for various low-level
+ * SIP tasks like sending messages.
+ */
+class SipHelper {
+    private static final String TAG = SipHelper.class.getSimpleName();
+
+    private SipStack mSipStack;
+    private SipProvider mSipProvider;
+    private AddressFactory mAddressFactory;
+    private HeaderFactory mHeaderFactory;
+    private MessageFactory mMessageFactory;
+
+    public SipHelper(SipStack sipStack, SipProvider sipProvider)
+            throws PeerUnavailableException {
+        mSipStack = sipStack;
+        mSipProvider = sipProvider;
+
+        SipFactory sipFactory = SipFactory.getInstance();
+        mAddressFactory = sipFactory.createAddressFactory();
+        mHeaderFactory = sipFactory.createHeaderFactory();
+        mMessageFactory = sipFactory.createMessageFactory();
+    }
+
+    private FromHeader createFromHeader(SipProfile profile, String tag)
+            throws ParseException {
+        return mHeaderFactory.createFromHeader(profile.getSipAddress(), tag);
+    }
+
+    private ToHeader createToHeader(SipProfile profile) throws ParseException {
+        return createToHeader(profile, null);
+    }
+
+    private ToHeader createToHeader(SipProfile profile, String tag)
+            throws ParseException {
+        return mHeaderFactory.createToHeader(profile.getSipAddress(), tag);
+    }
+
+    private CallIdHeader createCallIdHeader() {
+        return mSipProvider.getNewCallId();
+    }
+
+    private CSeqHeader createCSeqHeader(String method)
+            throws ParseException, InvalidArgumentException {
+        long sequence = (long) (Math.random() * 10000);
+        return mHeaderFactory.createCSeqHeader(sequence, method);
+    }
+
+    private MaxForwardsHeader createMaxForwardsHeader()
+            throws InvalidArgumentException {
+        return mHeaderFactory.createMaxForwardsHeader(70);
+    }
+
+    private MaxForwardsHeader createMaxForwardsHeader(int max)
+            throws InvalidArgumentException {
+        return mHeaderFactory.createMaxForwardsHeader(max);
+    }
+
+    private ListeningPoint getListeningPoint() throws SipException {
+        ListeningPoint lp = mSipProvider.getListeningPoint(ListeningPoint.UDP);
+        if (lp == null) lp = mSipProvider.getListeningPoint(ListeningPoint.TCP);
+        if (lp == null) {
+            ListeningPoint[] lps = mSipProvider.getListeningPoints();
+            if ((lps != null) && (lps.length > 0)) lp = lps[0];
+        }
+        if (lp == null) {
+            throw new SipException("no listening point is available");
+        }
+        return lp;
+    }
+
+    private List<ViaHeader> createViaHeaders()
+            throws ParseException, SipException {
+        List<ViaHeader> viaHeaders = new ArrayList<ViaHeader>(1);
+        ListeningPoint lp = getListeningPoint();
+        ViaHeader viaHeader = mHeaderFactory.createViaHeader(lp.getIPAddress(),
+                lp.getPort(), lp.getTransport(), null);
+        viaHeader.setRPort();
+        viaHeaders.add(viaHeader);
+        return viaHeaders;
+    }
+
+    private ContactHeader createContactHeader(SipProfile profile)
+            throws ParseException, SipException {
+        ListeningPoint lp = getListeningPoint();
+        SipURI contactURI =
+                createSipUri(profile.getUserName(), profile.getProtocol(), lp);
+
+        Address contactAddress = mAddressFactory.createAddress(contactURI);
+        contactAddress.setDisplayName(profile.getDisplayName());
+
+        return mHeaderFactory.createContactHeader(contactAddress);
+    }
+
+    private ContactHeader createWildcardContactHeader() {
+        ContactHeader contactHeader  = mHeaderFactory.createContactHeader();
+        contactHeader.setWildCard();
+        return contactHeader;
+    }
+
+    private SipURI createSipUri(String username, String transport,
+            ListeningPoint lp) throws ParseException {
+        SipURI uri = mAddressFactory.createSipURI(username, lp.getIPAddress());
+        try {
+            uri.setPort(lp.getPort());
+            uri.setTransportParam(transport);
+        } catch (InvalidArgumentException e) {
+            throw new RuntimeException(e);
+        }
+        return uri;
+    }
+
+    public ClientTransaction sendKeepAlive(SipProfile userProfile, String tag)
+            throws SipException {
+        try {
+            Request request = createRequest(Request.OPTIONS, userProfile, tag);
+
+            ClientTransaction clientTransaction =
+                    mSipProvider.getNewClientTransaction(request);
+            clientTransaction.sendRequest();
+            return clientTransaction;
+        } catch (Exception e) {
+            throw new SipException("sendKeepAlive()", e);
+        }
+    }
+
+    public ClientTransaction sendRegister(SipProfile userProfile, String tag,
+            int expiry) throws SipException {
+        try {
+            Request request = createRequest(Request.REGISTER, userProfile, tag);
+            if (expiry == 0) {
+                // remove all previous registrations by wildcard
+                // rfc3261#section-10.2.2
+                request.addHeader(createWildcardContactHeader());
+            } else {
+                request.addHeader(createContactHeader(userProfile));
+            }
+            request.addHeader(mHeaderFactory.createExpiresHeader(expiry));
+
+            ClientTransaction clientTransaction =
+                    mSipProvider.getNewClientTransaction(request);
+            clientTransaction.sendRequest();
+            return clientTransaction;
+        } catch (ParseException e) {
+            throw new SipException("sendRegister()", e);
+        }
+    }
+
+    private Request createRequest(String requestType, SipProfile userProfile,
+            String tag) throws ParseException, SipException {
+        FromHeader fromHeader = createFromHeader(userProfile, tag);
+        ToHeader toHeader = createToHeader(userProfile);
+        SipURI requestURI = mAddressFactory.createSipURI("sip:"
+                + userProfile.getSipDomain());
+        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);
+        Header userAgentHeader = mHeaderFactory.createHeader("User-Agent",
+                "SIPAUA/0.1.001");
+        request.addHeader(userAgentHeader);
+        return request;
+    }
+
+    public ClientTransaction handleChallenge(ResponseEvent responseEvent,
+            AccountManager accountManager) throws SipException {
+        AuthenticationHelper authenticationHelper =
+                ((SipStackExt) mSipStack).getAuthenticationHelper(
+                        accountManager, mHeaderFactory);
+        ClientTransaction tid = responseEvent.getClientTransaction();
+        ClientTransaction ct = authenticationHelper.handleChallenge(
+                responseEvent.getResponse(), tid, mSipProvider, 5);
+        ct.sendRequest();
+        return ct;
+    }
+
+    public ClientTransaction sendInvite(SipProfile caller, SipProfile callee,
+            SessionDescription 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.setContent(sessionDescription.getContent(),
+                    mHeaderFactory.createContentTypeHeader(
+                            "application", sessionDescription.getType()));
+
+            ClientTransaction clientTransaction =
+                    mSipProvider.getNewClientTransaction(request);
+            clientTransaction.sendRequest();
+            return clientTransaction;
+        } catch (ParseException e) {
+            throw new SipException("sendInvite()", e);
+        }
+    }
+
+    public ClientTransaction sendReinvite(Dialog dialog,
+            SessionDescription sessionDescription) throws SipException {
+        try {
+            Request request = dialog.createRequest(Request.INVITE);
+            request.setContent(sessionDescription.getContent(),
+                    mHeaderFactory.createContentTypeHeader(
+                            "application", sessionDescription.getType()));
+
+            ClientTransaction clientTransaction =
+                    mSipProvider.getNewClientTransaction(request);
+            dialog.sendRequest(clientTransaction);
+            return clientTransaction;
+        } catch (ParseException e) {
+            throw new SipException("sendReinvite()", e);
+        }
+    }
+
+    private ServerTransaction getServerTransaction(RequestEvent event)
+            throws SipException {
+        ServerTransaction transaction = event.getServerTransaction();
+        if (transaction == null) {
+            Request request = event.getRequest();
+            return mSipProvider.getNewServerTransaction(request);
+        } else {
+            return transaction;
+        }
+    }
+
+    /**
+     * @param event the INVITE request event
+     */
+    public ServerTransaction sendRinging(RequestEvent event, String tag)
+            throws SipException {
+        try {
+            Request request = event.getRequest();
+            ServerTransaction transaction = getServerTransaction(event);
+
+            Response response = mMessageFactory.createResponse(Response.RINGING,
+                    request);
+
+            ToHeader toHeader = (ToHeader) response.getHeader(ToHeader.NAME);
+            toHeader.setTag(tag);
+            response.addHeader(toHeader);
+            transaction.sendResponse(response);
+            return transaction;
+        } catch (ParseException e) {
+            throw new SipException("sendRinging()", e);
+        }
+    }
+
+    /**
+     * @param event the INVITE request event
+     */
+    public ServerTransaction sendInviteOk(RequestEvent event,
+            SipProfile localProfile, SessionDescription sessionDescription,
+            ServerTransaction inviteTransaction)
+            throws SipException {
+        try {
+            Request request = event.getRequest();
+            Response response = mMessageFactory.createResponse(Response.OK,
+                    request);
+            response.addHeader(createContactHeader(localProfile));
+            response.setContent(sessionDescription.getContent(),
+                    mHeaderFactory.createContentTypeHeader(
+                            "application", sessionDescription.getType()));
+
+            if (inviteTransaction == null) {
+                inviteTransaction = getServerTransaction(event);
+            }
+            if (inviteTransaction.getState() != TransactionState.COMPLETED) {
+                inviteTransaction.sendResponse(response);
+            }
+
+            return inviteTransaction;
+        } catch (ParseException e) {
+            throw new SipException("sendInviteOk()", e);
+        }
+    }
+
+    public void sendInviteBusyHere(RequestEvent event,
+            ServerTransaction inviteTransaction) throws SipException {
+        try {
+            Request request = event.getRequest();
+            Response response = mMessageFactory.createResponse(
+                    Response.BUSY_HERE, request);
+
+            if (inviteTransaction.getState() != TransactionState.COMPLETED) {
+                inviteTransaction.sendResponse(response);
+            }
+        } catch (ParseException e) {
+            throw new SipException("sendInviteBusyHere()", e);
+        }
+    }
+
+    /**
+     * @param event the INVITE ACK request event
+     */
+    public void sendInviteAck(ResponseEvent event, Dialog dialog)
+            throws SipException {
+        Response response = event.getResponse();
+        long cseq = ((CSeqHeader) response.getHeader(CSeqHeader.NAME))
+                .getSeqNumber();
+        dialog.sendAck(dialog.createAck(cseq));
+    }
+
+    public void sendBye(Dialog dialog) throws SipException {
+        Request byeRequest = dialog.createRequest(Request.BYE);
+        Log.d(TAG, "send BYE: " + byeRequest);
+        dialog.sendRequest(mSipProvider.getNewClientTransaction(byeRequest));
+    }
+
+    public void sendCancel(ClientTransaction inviteTransaction)
+            throws SipException {
+        Request cancelRequest = inviteTransaction.createCancel();
+        mSipProvider.getNewClientTransaction(cancelRequest).sendRequest();
+    }
+
+    public void sendResponse(RequestEvent event, int responseCode)
+            throws SipException {
+        try {
+            getServerTransaction(event).sendResponse(
+                    mMessageFactory.createResponse(
+                            responseCode, event.getRequest()));
+        } catch (ParseException e) {
+            throw new SipException("sendResponse()", e);
+        }
+    }
+
+    public void sendInviteRequestTerminated(Request inviteRequest,
+            ServerTransaction inviteTransaction) throws SipException {
+        try {
+            inviteTransaction.sendResponse(mMessageFactory.createResponse(
+                    Response.REQUEST_TERMINATED, inviteRequest));
+        } catch (ParseException e) {
+            throw new SipException("sendInviteRequestTerminated()", e);
+        }
+    }
+
+    public static String getCallId(EventObject event) {
+        if (event == null) return null;
+        if (event instanceof RequestEvent) {
+            return getCallId(((RequestEvent) event).getRequest());
+        } else if (event instanceof ResponseEvent) {
+            return getCallId(((ResponseEvent) event).getResponse());
+        } else if (event instanceof DialogTerminatedEvent) {
+            Dialog dialog = ((DialogTerminatedEvent) event).getDialog();
+            return getCallId(((DialogTerminatedEvent) event).getDialog());
+        } else if (event instanceof TransactionTerminatedEvent) {
+            TransactionTerminatedEvent e = (TransactionTerminatedEvent) event;
+            return getCallId(e.isServerTransaction()
+                    ? e.getServerTransaction()
+                    : e.getClientTransaction());
+        } else {
+            Object source = event.getSource();
+            if (source instanceof Transaction) {
+                return getCallId(((Transaction) source));
+            } else if (source instanceof Dialog) {
+                return getCallId((Dialog) source);
+            }
+        }
+        return "";
+    }
+
+    public static String getCallId(Transaction transaction) {
+        return ((transaction != null) ? getCallId(transaction.getRequest())
+                                      : "");
+    }
+
+    private static String getCallId(Message message) {
+        CallIdHeader callIdHeader =
+                (CallIdHeader) message.getHeader(CallIdHeader.NAME);
+        return callIdHeader.getCallId();
+    }
+
+    private static String getCallId(Dialog dialog) {
+        return dialog.getCallId().getCallId();
+    }
+}
diff --git a/services/java/com/android/server/sip/SipService.java b/services/java/com/android/server/sip/SipService.java
new file mode 100644
index 0000000..e905089
--- /dev/null
+++ b/services/java/com/android/server/sip/SipService.java
@@ -0,0 +1,1091 @@
+/*
+ * Copyright (C) 2010, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.sip;
+
+import 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.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.sip.ISipService;
+import android.net.sip.ISipSession;
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.net.sip.SipSessionAdapter;
+import android.net.sip.SipSessionState;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+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 javax.sip.SipException;
+
+/**
+ */
+public final class SipService extends ISipService.Stub {
+    private static final String TAG = "SipService";
+    private static final int EXPIRY_TIME = 3600;
+    private static final int SHORT_EXPIRY_TIME = 10;
+    private static final int MIN_EXPIRY_TIME = 60;
+
+    private Context mContext;
+    private String mLocalIp;
+    private String mNetworkType;
+    private boolean mConnected;
+    private WakeupTimer mTimer;
+    private WifiManager.WifiLock mWifiLock;
+
+    // SipProfile URI --> group
+    private Map<String, SipSessionGroupExt> mSipGroups =
+            new HashMap<String, SipSessionGroupExt>();
+
+    // session ID --> session
+    private Map<String, ISipSession> mPendingSessions =
+            new HashMap<String, ISipSession>();
+
+    private ConnectivityReceiver mConnectivityReceiver;
+
+    public SipService(Context context) {
+        Log.v(TAG, " service started!");
+        mContext = context;
+        mConnectivityReceiver = new ConnectivityReceiver();
+        context.registerReceiver(mConnectivityReceiver,
+                new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+
+        mTimer = new WakeupTimer(context);
+    }
+
+    public synchronized SipProfile[] getListOfProfiles() {
+        SipProfile[] profiles = new SipProfile[mSipGroups.size()];
+        int i = 0;
+        for (SipSessionGroupExt group : mSipGroups.values()) {
+            profiles[i++] = group.getLocalProfile();
+        }
+        return profiles;
+    }
+
+    public void open(SipProfile localProfile) {
+        if (localProfile.getAutoRegistration()) {
+            openToReceiveCalls(localProfile);
+        } else {
+            openToMakeCalls(localProfile);
+        }
+    }
+
+    private void openToMakeCalls(SipProfile localProfile) {
+        try {
+            createGroup(localProfile);
+        } catch (SipException e) {
+            Log.e(TAG, "openToMakeCalls()", e);
+            // TODO: how to send the exception back
+        }
+    }
+
+    private void openToReceiveCalls(SipProfile localProfile) {
+        open3(localProfile, SipManager.SIP_INCOMING_CALL_ACTION, null);
+    }
+
+    public synchronized void open3(SipProfile localProfile,
+            String incomingCallBroadcastAction, ISipSessionListener listener) {
+        if (TextUtils.isEmpty(incomingCallBroadcastAction)) {
+            throw new RuntimeException(
+                    "empty broadcast action for incoming call");
+        }
+        Log.v(TAG, "open3: " + localProfile.getUriString() + ": "
+                + incomingCallBroadcastAction + ": " + listener);
+        try {
+            SipSessionGroupExt group = createGroup(localProfile,
+                    incomingCallBroadcastAction, listener);
+            if (localProfile.getAutoRegistration()) {
+                group.openToReceiveCalls();
+                if (isWifiOn()) grabWifiLock();
+            }
+        } catch (SipException e) {
+            Log.e(TAG, "openToReceiveCalls()", e);
+            // TODO: how to send the exception back
+        }
+    }
+
+    public synchronized void close(String localProfileUri) {
+        SipSessionGroupExt group = mSipGroups.remove(localProfileUri);
+        if (group != null) {
+            notifyProfileRemoved(group.getLocalProfile());
+            group.closeToNotReceiveCalls();
+            if (isWifiOn() && !anyOpened()) releaseWifiLock();
+        }
+    }
+
+    public synchronized boolean isOpened(String localProfileUri) {
+        SipSessionGroupExt group = mSipGroups.get(localProfileUri);
+        return ((group != null) ? group.isOpened() : false);
+    }
+
+    public synchronized boolean isRegistered(String localProfileUri) {
+        SipSessionGroupExt group = mSipGroups.get(localProfileUri);
+        return ((group != null) ? group.isRegistered() : false);
+    }
+
+    public synchronized void setRegistrationListener(String localProfileUri,
+            ISipSessionListener listener) {
+        SipSessionGroupExt group = mSipGroups.get(localProfileUri);
+        if (group != null) group.setListener(listener);
+    }
+
+    public synchronized ISipSession createSession(SipProfile localProfile,
+            ISipSessionListener listener) {
+        if (!mConnected) return null;
+        try {
+            SipSessionGroupExt group = createGroup(localProfile);
+            return group.createSession(listener);
+        } catch (SipException e) {
+            Log.w(TAG, "createSession()", e);
+            return null;
+        }
+    }
+
+    public synchronized ISipSession getPendingSession(String callId) {
+        if (callId == null) return null;
+        return mPendingSessions.get(callId);
+    }
+
+    private String determineLocalIp() {
+        try {
+            DatagramSocket s = new DatagramSocket();
+            s.connect(InetAddress.getByName("192.168.1.1"), 80);
+            return s.getLocalAddress().getHostAddress();
+        } catch (IOException e) {
+            Log.w(TAG, "determineLocalIp()", e);
+            // dont do anything; there should be a connectivity change going
+            return null;
+        }
+    }
+
+    private SipSessionGroupExt createGroup(SipProfile localProfile)
+            throws SipException {
+        String key = localProfile.getUriString();
+        SipSessionGroupExt group = mSipGroups.get(key);
+        if (group == null) {
+            group = new SipSessionGroupExt(localProfile, null, null);
+            mSipGroups.put(key, group);
+            notifyProfileAdded(localProfile);
+        }
+        return group;
+    }
+
+    private SipSessionGroupExt createGroup(SipProfile localProfile,
+            String incomingCallBroadcastAction, ISipSessionListener listener)
+            throws SipException {
+        String key = localProfile.getUriString();
+        SipSessionGroupExt group = mSipGroups.get(key);
+        if (group != null) {
+            group.setIncomingCallBroadcastAction(
+                    incomingCallBroadcastAction);
+            group.setListener(listener);
+        } else {
+            group = new SipSessionGroupExt(localProfile,
+                    incomingCallBroadcastAction, listener);
+            mSipGroups.put(key, group);
+            notifyProfileAdded(localProfile);
+        }
+        return group;
+    }
+
+    private void notifyProfileAdded(SipProfile localProfile) {
+        Log.d(TAG, "notify: profile added: " + localProfile);
+        Intent intent = new Intent(SipManager.SIP_ADD_PHONE_ACTION);
+        intent.putExtra(SipManager.LOCAL_URI_KEY, localProfile.getUriString());
+        mContext.sendBroadcast(intent);
+    }
+
+    private void notifyProfileRemoved(SipProfile localProfile) {
+        Log.d(TAG, "notify: profile removed: " + localProfile);
+        Intent intent = new Intent(SipManager.SIP_REMOVE_PHONE_ACTION);
+        intent.putExtra(SipManager.LOCAL_URI_KEY, localProfile.getUriString());
+        mContext.sendBroadcast(intent);
+    }
+
+    private boolean anyOpened() {
+        for (SipSessionGroupExt group : mSipGroups.values()) {
+            if (group.isOpened()) return true;
+        }
+        return false;
+    }
+
+    private void grabWifiLock() {
+        if (mWifiLock == null) {
+            Log.v(TAG, "acquire wifi lock");
+            mWifiLock = ((WifiManager)
+                    mContext.getSystemService(Context.WIFI_SERVICE))
+                    .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
+            mWifiLock.acquire();
+        }
+    }
+
+    private void releaseWifiLock() {
+        if (mWifiLock != null) {
+            Log.v(TAG, "release wifi lock");
+            mWifiLock.release();
+            mWifiLock = null;
+        }
+    }
+
+    private boolean isWifiOn() {
+        return "WIFI".equalsIgnoreCase(mNetworkType);
+        //return (mConnected && "WIFI".equalsIgnoreCase(mNetworkType));
+    }
+
+    private synchronized void onConnectivityChanged(
+            String type, boolean connected) {
+        Log.v(TAG, "onConnectivityChanged(): "
+                + mNetworkType + (mConnected? " CONNECTED" : " DISCONNECTED")
+                + " --> " + type + (connected? " CONNECTED" : " DISCONNECTED"));
+
+        boolean sameType = type.equals(mNetworkType);
+        if (!sameType && !connected) return;
+
+        boolean wasWifi = "WIFI".equalsIgnoreCase(mNetworkType);
+        boolean isWifi = "WIFI".equalsIgnoreCase(type);
+        boolean wifiOff = (isWifi && !connected) || (wasWifi && !sameType);
+        boolean wifiOn = isWifi && connected;
+        if (wifiOff) {
+            releaseWifiLock();
+        } else if (wifiOn) {
+            if (anyOpened()) grabWifiLock();
+        }
+
+        try {
+            boolean wasConnected = mConnected;
+            mNetworkType = type;
+            mConnected = connected;
+
+            if (wasConnected) {
+                mLocalIp = null;
+                for (SipSessionGroupExt group : mSipGroups.values()) {
+                    group.onConnectivityChanged(false);
+                }
+            }
+
+            if (connected) {
+                mLocalIp = determineLocalIp();
+                for (SipSessionGroupExt group : mSipGroups.values()) {
+                    group.onConnectivityChanged(true);
+                }
+            }
+
+        } catch (SipException e) {
+            Log.e(TAG, "onConnectivityChanged()", e);
+        }
+    }
+
+    private synchronized void addPendingSession(ISipSession session) {
+        try {
+            mPendingSessions.put(session.getCallId(), session);
+        } catch (RemoteException e) {
+            // should not happen with a local call
+            Log.e(TAG, "addPendingSession()", e);
+        }
+    }
+
+    private class SipSessionGroupExt extends SipSessionAdapter {
+        private SipSessionGroup mSipGroup;
+        private String mIncomingCallBroadcastAction;
+        private boolean mOpened;
+
+        private AutoRegistrationProcess mAutoRegistration =
+                new AutoRegistrationProcess();
+
+        public SipSessionGroupExt(SipProfile localProfile,
+                String incomingCallBroadcastAction,
+                ISipSessionListener listener) throws SipException {
+            String password = localProfile.getPassword();
+            SipProfile p = duplicate(localProfile);
+            mSipGroup = createSipSessionGroup(mLocalIp, p, password);
+            mIncomingCallBroadcastAction = incomingCallBroadcastAction;
+            mAutoRegistration.setListener(listener);
+        }
+
+        public SipProfile getLocalProfile() {
+            return mSipGroup.getLocalProfile();
+        }
+
+        // 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
+        private SipSessionGroup createSipSessionGroup(String localIp,
+                SipProfile localProfile, String password) throws SipException {
+            try {
+                return new SipSessionGroup(localIp, localProfile, password);
+            } catch (IOException e) {
+                // network disconnected
+                Log.w(TAG, "createSipSessionGroup(): network disconnected?");
+                if (localIp != null) {
+                    return createSipSessionGroup(null, localProfile, password);
+                } else {
+                    // recursive
+                    Log.wtf(TAG, "impossible!");
+                    throw new RuntimeException("createSipSessionGroup");
+                }
+            }
+        }
+
+        private SipProfile duplicate(SipProfile p) {
+            try {
+                return new SipProfile.Builder(p.getUserName(), p.getSipDomain())
+                        .setProfileName(p.getProfileName())
+                        .setPassword("*")
+                        .setPort(p.getPort())
+                        .setProtocol(p.getProtocol())
+                        .setOutboundProxy(p.getProxyAddress())
+                        .setSendKeepAlive(p.getSendKeepAlive())
+                        .setAutoRegistration(p.getAutoRegistration())
+                        .setDisplayName(p.getDisplayName())
+                        .build();
+            } catch (Exception e) {
+                Log.wtf(TAG, "duplicate()", e);
+                throw new RuntimeException("duplicate profile", e);
+            }
+        }
+
+        public void setListener(ISipSessionListener listener) {
+            mAutoRegistration.setListener(listener);
+        }
+
+        public void setIncomingCallBroadcastAction(String action) {
+            mIncomingCallBroadcastAction = action;
+        }
+
+        public void openToReceiveCalls() throws SipException {
+            mOpened = true;
+            if (mConnected) {
+                mSipGroup.openToReceiveCalls(this);
+                mAutoRegistration.start(mSipGroup);
+            }
+            Log.v(TAG, "  openToReceiveCalls: " + getUri() + ": "
+                    + mIncomingCallBroadcastAction);
+        }
+
+        public void onConnectivityChanged(boolean connected)
+                throws SipException {
+            if (connected) {
+                resetGroup(mLocalIp);
+                if (mOpened) openToReceiveCalls();
+            } else {
+                // close mSipGroup but remember mOpened
+                Log.v(TAG, "  close auto reg temporarily: " + getUri() + ": "
+                        + mIncomingCallBroadcastAction);
+                mSipGroup.close();
+                mAutoRegistration.stop();
+            }
+        }
+
+        private void resetGroup(String localIp) throws SipException {
+            try {
+                mSipGroup.reset(localIp);
+            } catch (IOException e) {
+                // network disconnected
+                Log.w(TAG, "resetGroup(): network disconnected?");
+                if (localIp != null) {
+                    resetGroup(null); // reset w/o local IP
+                } else {
+                    // recursive
+                    Log.wtf(TAG, "impossible!");
+                    throw new RuntimeException("resetGroup");
+                }
+            }
+        }
+
+        public void closeToNotReceiveCalls() {
+            mOpened = false;
+            mSipGroup.closeToNotReceiveCalls();
+            mAutoRegistration.stop();
+            Log.v(TAG, "   close: " + getUri() + ": "
+                    + mIncomingCallBroadcastAction);
+        }
+
+        public ISipSession createSession(ISipSessionListener listener) {
+            return mSipGroup.createSession(listener);
+        }
+
+        @Override
+        public void onRinging(ISipSession session, SipProfile caller,
+                byte[] sessionDescription) {
+            synchronized (SipService.this) {
+                try {
+                    if (!isRegistered()) {
+                        session.endCall();
+                        return;
+                    }
+
+                    // send out incoming call broadcast
+                    Log.d(TAG, " ringing~~ " + getUri() + ": " + caller.getUri()
+                            + ": " + session.getCallId());
+                    addPendingSession(session);
+                    Intent intent = SipManager.createIncomingCallBroadcast(
+                            mIncomingCallBroadcastAction, session.getCallId(),
+                            sessionDescription);
+                    Log.d(TAG, "   send out intent: " + intent);
+                    mContext.sendBroadcast(intent);
+                } catch (RemoteException e) {
+                    // should never happen with a local call
+                    Log.e(TAG, "processCall()", e);
+                }
+            }
+        }
+
+        @Override
+        public void onError(ISipSession session, String errorClass,
+                String message) {
+            Log.v(TAG, "sip session error: " + errorClass + ": " + message);
+        }
+
+        public boolean isOpened() {
+            return mOpened;
+        }
+
+        public boolean isRegistered() {
+            return mAutoRegistration.isRegistered();
+        }
+
+        private String getUri() {
+            return mSipGroup.getLocalProfileUri();
+        }
+    }
+
+    private class KeepAliveProcess implements Runnable {
+        private static final String TAG = "\\KEEPALIVE/";
+        private static final int INTERVAL = 15;
+        private SipSessionGroup.SipSessionImpl mSession;
+
+        public KeepAliveProcess(SipSessionGroup.SipSessionImpl session) {
+            mSession = session;
+        }
+
+        public void start() {
+            mTimer.set(INTERVAL * 1000, this);
+        }
+
+        public void run() {
+            synchronized (SipService.this) {
+                SipSessionGroup.SipSessionImpl session = mSession.duplicate();
+                Log.d(TAG, "  ~~~ keepalive");
+                mTimer.cancel(this);
+                session.sendKeepAlive();
+                if (session.isReRegisterRequired()) {
+                    mSession.register(EXPIRY_TIME);
+                } else {
+                    mTimer.set(INTERVAL * 1000, this);
+                }
+            }
+        }
+
+        public void stop() {
+            mTimer.cancel(this);
+        }
+    }
+
+    private class AutoRegistrationProcess extends SipSessionAdapter
+            implements Runnable {
+        private SipSessionGroup.SipSessionImpl mSession;
+        private SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
+        private KeepAliveProcess mKeepAliveProcess;
+        private int mBackoff = 1;
+        private boolean mRegistered;
+        private long mExpiryTime;
+
+        private String getAction() {
+            return toString();
+        }
+
+        public void start(SipSessionGroup group) {
+            if (mSession == null) {
+                mBackoff = 1;
+                mSession = (SipSessionGroup.SipSessionImpl)
+                        group.createSession(this);
+                // return right away if no active network connection.
+                if (mSession == null) return;
+
+                // 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
+                mSession.unregister();
+                Log.v(TAG, "start AutoRegistrationProcess for "
+                        + mSession.getLocalProfile().getUriString());
+            }
+        }
+
+        public void stop() {
+            if (mSession == null) return;
+            if (mConnected) mSession.unregister();
+            mTimer.cancel(this);
+            if (mKeepAliveProcess != null) {
+                mKeepAliveProcess.stop();
+                mKeepAliveProcess = null;
+            }
+            mSession = null;
+            mRegistered = false;
+        }
+
+        private boolean isStopped() {
+            return (mSession == null);
+        }
+
+        public void setListener(ISipSessionListener listener) {
+            Log.v(TAG, "setListener(): " + listener);
+            mProxy.setListener(listener);
+            if (mSession == null) return;
+
+            try {
+                if ((mSession != null) && SipSessionState.REGISTERING.equals(
+                        mSession.getState())) {
+                    mProxy.onRegistering(mSession);
+                } else if (mRegistered) {
+                    int duration = (int)
+                            (mExpiryTime - SystemClock.elapsedRealtime());
+                    mProxy.onRegistrationDone(mSession, duration);
+                }
+            } catch (Throwable t) {
+                Log.w(TAG, "setListener(): " + t);
+            }
+        }
+
+        public boolean isRegistered() {
+            return mRegistered;
+        }
+
+        public void run() {
+            Log.v(TAG, "  ~~~ registering");
+            synchronized (SipService.this) {
+                if (mConnected && !isStopped()) mSession.register(EXPIRY_TIME);
+            }
+        }
+
+        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) {
+            Log.v(TAG, "Refresh registration " + duration + "s later.");
+            mTimer.cancel(this);
+            mTimer.set(duration * 1000, this);
+        }
+
+        private int backoffDuration() {
+            int duration = SHORT_EXPIRY_TIME * mBackoff;
+            if (duration > 3600) {
+                duration = 3600;
+            } else {
+                mBackoff *= 2;
+            }
+            return duration;
+        }
+
+        @Override
+        public void onRegistering(ISipSession session) {
+            Log.v(TAG, "onRegistering(): " + session + ": " + mSession);
+            synchronized (SipService.this) {
+                if (!isStopped() && (session != mSession)) return;
+                mRegistered = false;
+                try {
+                    mProxy.onRegistering(session);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onRegistering()", t);
+                }
+            }
+        }
+
+        @Override
+        public void onRegistrationDone(ISipSession session, int duration) {
+            Log.v(TAG, "onRegistrationDone(): " + session + ": " + mSession);
+            synchronized (SipService.this) {
+                if (!isStopped() && (session != mSession)) return;
+                try {
+                    mProxy.onRegistrationDone(session, duration);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onRegistrationDone()", t);
+                }
+                if (isStopped()) return;
+
+                if (duration > 0) {
+                    mSession.clearReRegisterRequired();
+                    mExpiryTime = SystemClock.elapsedRealtime()
+                            + (duration * 1000);
+
+                    if (!mRegistered) {
+                        mRegistered = true;
+                        // allow some overlap to avoid call drop during renew
+                        duration -= MIN_EXPIRY_TIME;
+                        if (duration < MIN_EXPIRY_TIME) {
+                            duration = MIN_EXPIRY_TIME;
+                        }
+                        restart(duration);
+
+                        if (isBehindNAT(mLocalIp) ||
+                                mSession.getLocalProfile().getSendKeepAlive()) {
+                            if (mKeepAliveProcess == null) {
+                                mKeepAliveProcess =
+                                        new KeepAliveProcess(mSession);
+                            }
+                            mKeepAliveProcess.start();
+                        }
+                    }
+                } else {
+                    mRegistered = false;
+                    mExpiryTime = -1L;
+                    Log.v(TAG, "Refresh registration immediately");
+                    run();
+                }
+            }
+        }
+
+        @Override
+        public void onRegistrationFailed(ISipSession session, String className,
+                String message) {
+            Log.v(TAG, "onRegistrationFailed(): " + session + ": " + mSession
+                    + ": " + className + ": " + message);
+            synchronized (SipService.this) {
+                if (!isStopped() && (session != mSession)) return;
+                try {
+                    mProxy.onRegistrationFailed(session, className, message);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onRegistrationFailed(): " + t);
+                }
+
+                if (!isStopped()) onError();
+            }
+        }
+
+        @Override
+        public void onRegistrationTimeout(ISipSession session) {
+            Log.v(TAG, "onRegistrationTimeout(): " + session + ": " + mSession);
+            synchronized (SipService.this) {
+                if (!isStopped() && (session != mSession)) return;
+                try {
+                    mProxy.onRegistrationTimeout(session);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onRegistrationTimeout(): " + t);
+                }
+
+                if (!isStopped()) {
+                    mRegistered = false;
+                    onError();
+                }
+            }
+        }
+
+        private void onError() {
+            mRegistered = false;
+            restart(backoffDuration());
+            if (mKeepAliveProcess != null) {
+                mKeepAliveProcess.stop();
+                mKeepAliveProcess = null;
+            }
+        }
+    }
+
+    private class ConnectivityReceiver extends BroadcastReceiver {
+        private Timer mTimer = new Timer();
+        private MyTimerTask mTask;
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+                Bundle b = intent.getExtras();
+                if (b != null) {
+                    NetworkInfo netInfo = (NetworkInfo)
+                            b.get(ConnectivityManager.EXTRA_NETWORK_INFO);
+                    String type = netInfo.getTypeName();
+                    NetworkInfo.State state = netInfo.getState();
+                    if (state == NetworkInfo.State.CONNECTED) {
+                        Log.v(TAG, "Connectivity alert: CONNECTED " + type);
+                        onChanged(type, true);
+                    } else if (state == NetworkInfo.State.DISCONNECTED) {
+                        Log.v(TAG, "Connectivity alert: DISCONNECTED " + type);
+                        onChanged(type, false);
+                    } else {
+                        Log.d(TAG, "Connectivity alert not processed: " + state
+                                + " " + type);
+                    }
+                }
+            }
+        }
+
+        private void onChanged(String type, boolean connected) {
+            synchronized (SipService.this) {
+                // When turning on WIFI, it needs some time for network
+                // connectivity to get stabile so we defer good news (because
+                // we want to skip the interim ones) but deliver bad news
+                // immediately
+                if (connected) {
+                    if (mTask != null) mTask.cancel();
+                    mTask = new MyTimerTask(type, connected);
+                    mTimer.schedule(mTask, 3 * 1000L);
+                    // TODO: hold wakup lock so that we can finish change before
+                    // the device goes to sleep
+                } else {
+                    if ((mTask != null) && mTask.mNetworkType.equals(type)) {
+                        mTask.cancel();
+                    }
+                    onConnectivityChanged(type, false);
+                }
+            }
+        }
+
+        private class MyTimerTask extends TimerTask {
+            private boolean mConnected;
+            private String mNetworkType;
+
+            public MyTimerTask(String type, boolean connected) {
+                mNetworkType = type;
+                mConnected = connected;
+            }
+
+            @Override
+            public void run() {
+                synchronized (SipService.this) {
+                    if (mTask != this) {
+                        Log.w(TAG, "  unexpected task: " + mNetworkType
+                                + (mConnected ? " CONNECTED" : "DISCONNECTED"));
+                        return;
+                    }
+                    mTask = null;
+                    Log.v(TAG, " deliver change for " + mNetworkType
+                            + (mConnected ? " CONNECTED" : "DISCONNECTED"));
+                    onConnectivityChanged(mNetworkType, mConnected);
+                }
+            }
+        }
+    }
+
+    // TODO: clean up pending SipSession(s) periodically
+
+
+    /**
+     * 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;
+            Log.v(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;
+            Log.v(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;
+            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();
+                    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();
+            }
+            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) {
+            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) {
+            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;
+                Log.d(TAG, "execute " + event);
+
+                event.mLastTriggerTime = event.mTriggerTime;
+                event.mTriggerTime += event.mPeriod;
+
+                // run the callback in a new thread to prevent deadlock
+                new Thread(event.mCallback).start();
+            }
+            Log.d(TAG, "after timeout execution");
+            printQueue();
+            scheduleNext();
+        }
+
+        private String getAction() {
+            return toString();
+        }
+
+        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 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);
+        }
+    }
+}
diff --git a/services/java/com/android/server/sip/SipSessionGroup.java b/services/java/com/android/server/sip/SipSessionGroup.java
new file mode 100644
index 0000000..db3f536
--- /dev/null
+++ b/services/java/com/android/server/sip/SipSessionGroup.java
@@ -0,0 +1,1081 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.sip;
+
+import gov.nist.javax.sip.clientauthutils.AccountManager;
+import gov.nist.javax.sip.clientauthutils.UserCredentials;
+import gov.nist.javax.sip.header.SIPHeaderNames;
+import gov.nist.javax.sip.header.WWWAuthenticate;
+
+import android.net.sip.ISipSession;
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SessionDescription;
+import android.net.sip.SipProfile;
+import android.net.sip.SipSessionAdapter;
+import android.net.sip.SipSessionState;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.text.ParseException;
+import java.util.Collection;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TooManyListenersException;
+
+import javax.sip.ClientTransaction;
+import javax.sip.Dialog;
+import javax.sip.DialogTerminatedEvent;
+import javax.sip.IOExceptionEvent;
+import javax.sip.InvalidArgumentException;
+import javax.sip.ListeningPoint;
+import javax.sip.RequestEvent;
+import javax.sip.ResponseEvent;
+import javax.sip.ServerTransaction;
+import javax.sip.SipException;
+import javax.sip.SipFactory;
+import javax.sip.SipListener;
+import javax.sip.SipProvider;
+import javax.sip.SipStack;
+import javax.sip.TimeoutEvent;
+import javax.sip.Transaction;
+import javax.sip.TransactionState;
+import javax.sip.TransactionTerminatedEvent;
+import javax.sip.address.Address;
+import javax.sip.address.SipURI;
+import javax.sip.header.CSeqHeader;
+import javax.sip.header.ExpiresHeader;
+import javax.sip.header.FromHeader;
+import javax.sip.header.MinExpiresHeader;
+import javax.sip.header.ViaHeader;
+import javax.sip.message.Message;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+
+/**
+ * Manages {@link ISipSession}'s for a SIP account.
+ */
+class SipSessionGroup implements SipListener {
+    private static final String TAG = "SipSession";
+    private static final String ANONYMOUS = "anonymous";
+    private static final int EXPIRY_TIME = 3600;
+
+    private static final EventObject DEREGISTER = new EventObject("Deregister");
+    private static final EventObject END_CALL = new EventObject("End call");
+    private static final EventObject HOLD_CALL = new EventObject("Hold call");
+    private static final EventObject CONTINUE_CALL
+            = new EventObject("Continue call");
+
+    private final SipProfile mLocalProfile;
+    private final String mPassword;
+
+    private SipStack mSipStack;
+    private SipHelper mSipHelper;
+    private String mLastNonce;
+    private int mRPort;
+
+    // session that processes INVITE requests
+    private SipSessionImpl mCallReceiverSession;
+    private String mLocalIp;
+
+    // call-id-to-SipSession map
+    private Map<String, SipSessionImpl> mSessionMap =
+            new HashMap<String, SipSessionImpl>();
+
+    /**
+     * @param myself the local profile with password crossed out
+     * @param password the password of the profile
+     * @throws IOException if cannot assign requested address
+     */
+    public SipSessionGroup(String localIp, SipProfile myself, String password)
+            throws SipException, IOException {
+        mLocalProfile = myself;
+        mPassword = password;
+        reset(localIp);
+    }
+
+    void reset(String localIp) throws SipException, IOException {
+        mLocalIp = localIp;
+        if (localIp == null) return;
+
+        SipProfile myself = mLocalProfile;
+        SipFactory sipFactory = SipFactory.getInstance();
+        Properties properties = new Properties();
+        properties.setProperty("javax.sip.STACK_NAME", getStackName());
+        String outboundProxy = myself.getProxyAddress();
+        if (!TextUtils.isEmpty(outboundProxy)) {
+            properties.setProperty("javax.sip.OUTBOUND_PROXY", outboundProxy
+                    + ":" + myself.getPort() + "/" + myself.getProtocol());
+        }
+        SipStack stack = mSipStack = sipFactory.createSipStack(properties);
+
+        try {
+            SipProvider provider = stack.createSipProvider(
+                    stack.createListeningPoint(localIp, allocateLocalPort(),
+                            myself.getProtocol()));
+            provider.addSipListener(this);
+            mSipHelper = new SipHelper(stack, provider);
+        } catch (InvalidArgumentException e) {
+            throw new IOException(e.getMessage());
+        } catch (TooManyListenersException e) {
+            // must never happen
+            throw new SipException("SipSessionGroup constructor", e);
+        }
+        Log.d(TAG, " start stack for " + myself.getUriString());
+        stack.start();
+
+        mLastNonce = null;
+        mCallReceiverSession = null;
+        mSessionMap.clear();
+    }
+
+    public SipProfile getLocalProfile() {
+        return mLocalProfile;
+    }
+
+    public String getLocalProfileUri() {
+        return mLocalProfile.getUriString();
+    }
+
+    private String getStackName() {
+        return "stack" + System.currentTimeMillis();
+    }
+
+    public synchronized void close() {
+        Log.d(TAG, " close stack for " + mLocalProfile.getUriString());
+        mSessionMap.clear();
+        closeToNotReceiveCalls();
+        if (mSipStack != null) {
+            mSipStack.stop();
+            mSipStack = null;
+            mSipHelper = null;
+        }
+    }
+
+    public synchronized boolean isClosed() {
+        return (mSipStack == null);
+    }
+
+    // For internal use, require listener not to block in callbacks.
+    public synchronized void openToReceiveCalls(ISipSessionListener listener) {
+        if (mCallReceiverSession == null) {
+            mCallReceiverSession = new SipSessionCallReceiverImpl(listener);
+        } else {
+            mCallReceiverSession.setListener(listener);
+        }
+    }
+
+    public synchronized void closeToNotReceiveCalls() {
+        mCallReceiverSession = null;
+    }
+
+    public ISipSession createSession(ISipSessionListener listener) {
+        return (isClosed() ? null : new SipSessionImpl(listener));
+    }
+
+    private static int allocateLocalPort() throws SipException {
+        try {
+            DatagramSocket s = new DatagramSocket();
+            int localPort = s.getLocalPort();
+            s.close();
+            return localPort;
+        } catch (IOException e) {
+            throw new SipException("allocateLocalPort()", e);
+        }
+    }
+
+    private synchronized SipSessionImpl getSipSession(EventObject event) {
+        String key = SipHelper.getCallId(event);
+        Log.d(TAG, " sesssion key from event: " + key);
+        Log.d(TAG, " active sessions:");
+        for (String k : mSessionMap.keySet()) {
+            Log.d(TAG, "   .....  '" + k + "': " + mSessionMap.get(k));
+        }
+        SipSessionImpl session = mSessionMap.get(key);
+        return ((session != null) ? session : mCallReceiverSession);
+    }
+
+    private synchronized void addSipSession(SipSessionImpl newSession) {
+        removeSipSession(newSession);
+        String key = newSession.getCallId();
+        Log.d(TAG, " +++++  add a session with key:  '" + key + "'");
+        mSessionMap.put(key, newSession);
+        for (String k : mSessionMap.keySet()) {
+            Log.d(TAG, "   .....  " + k + ": " + mSessionMap.get(k));
+        }
+    }
+
+    private synchronized void removeSipSession(SipSessionImpl session) {
+        if (session == mCallReceiverSession) return;
+        String key = session.getCallId();
+        SipSessionImpl s = mSessionMap.remove(key);
+        // sanity check
+        if ((s != null) && (s != session)) {
+            Log.w(TAG, "session " + session + " is not associated with key '"
+                    + key + "'");
+            mSessionMap.put(key, s);
+            for (Map.Entry<String, SipSessionImpl> entry
+                    : mSessionMap.entrySet()) {
+                if (entry.getValue() == s) {
+                    key = entry.getKey();
+                    mSessionMap.remove(key);
+                }
+            }
+        }
+        Log.d(TAG, "   remove session " + session + " with key '" + key + "'");
+
+        for (String k : mSessionMap.keySet()) {
+            Log.d(TAG, "   .....  " + k + ": " + mSessionMap.get(k));
+        }
+    }
+
+    public void processRequest(RequestEvent event) {
+        process(event);
+    }
+
+    public void processResponse(ResponseEvent event) {
+        process(event);
+    }
+
+    public void processIOException(IOExceptionEvent event) {
+        process(event);
+    }
+
+    public void processTimeout(TimeoutEvent event) {
+        process(event);
+    }
+
+    public void processTransactionTerminated(TransactionTerminatedEvent event) {
+        process(event);
+    }
+
+    public void processDialogTerminated(DialogTerminatedEvent event) {
+        process(event);
+    }
+
+    private synchronized void process(EventObject event) {
+        SipSessionImpl session = getSipSession(event);
+        try {
+            if ((session != null) && session.process(event)) {
+                Log.d(TAG, " ~~~~~   new state: " + session.mState);
+            } else {
+                Log.d(TAG, "event not processed: " + event);
+            }
+        } catch (Throwable e) {
+            Log.e(TAG, "event process error: " + event, e);
+            session.onError(e);
+        }
+    }
+
+    private class SipSessionCallReceiverImpl extends SipSessionImpl {
+        public SipSessionCallReceiverImpl(ISipSessionListener listener) {
+            super(listener);
+        }
+
+        public boolean process(EventObject evt) throws SipException {
+            Log.d(TAG, " ~~~~~   " + this + ": " + mState + ": processing "
+                    + log(evt));
+            if (isRequestEvent(Request.INVITE, evt)) {
+                RequestEvent event = (RequestEvent) evt;
+                SipSessionImpl newSession = new SipSessionImpl(mProxy);
+                newSession.mServerTransaction = mSipHelper.sendRinging(event,
+                        generateTag());
+                newSession.mDialog = newSession.mServerTransaction.getDialog();
+                newSession.mInviteReceived = event;
+                newSession.mPeerProfile = createPeerProfile(event.getRequest());
+                newSession.mState = SipSessionState.INCOMING_CALL;
+                newSession.mPeerSessionDescription =
+                        event.getRequest().getRawContent();
+                addSipSession(newSession);
+                mProxy.onRinging(newSession, newSession.mPeerProfile,
+                        newSession.mPeerSessionDescription);
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    class SipSessionImpl extends ISipSession.Stub {
+        SipProfile mPeerProfile;
+        SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
+        SipSessionState mState = SipSessionState.READY_TO_CALL;
+        RequestEvent mInviteReceived;
+        Dialog mDialog;
+        ServerTransaction mServerTransaction;
+        ClientTransaction mClientTransaction;
+        byte[] mPeerSessionDescription;
+        boolean mInCall;
+        boolean mReRegisterFlag = false;
+
+        public SipSessionImpl(ISipSessionListener listener) {
+            setListener(listener);
+        }
+
+        SipSessionImpl duplicate() {
+            return new SipSessionImpl(mProxy.getListener());
+        }
+
+        private void reset() {
+            mInCall = false;
+            removeSipSession(this);
+            mPeerProfile = null;
+            mState = SipSessionState.READY_TO_CALL;
+            mInviteReceived = null;
+            mDialog = null;
+            mServerTransaction = null;
+            mClientTransaction = null;
+            mPeerSessionDescription = null;
+        }
+
+        public boolean isInCall() {
+            return mInCall;
+        }
+
+        public String getLocalIp() {
+            return mLocalIp;
+        }
+
+        public SipProfile getLocalProfile() {
+            return mLocalProfile;
+        }
+
+        public SipProfile getPeerProfile() {
+            return mPeerProfile;
+        }
+
+        public String getCallId() {
+            return SipHelper.getCallId(getTransaction());
+        }
+
+        private Transaction getTransaction() {
+            if (mClientTransaction != null) return mClientTransaction;
+            if (mServerTransaction != null) return mServerTransaction;
+            return null;
+        }
+
+        public String getState() {
+            return mState.toString();
+        }
+
+        public void setListener(ISipSessionListener listener) {
+            mProxy.setListener((listener instanceof SipSessionListenerProxy)
+                    ? ((SipSessionListenerProxy) listener).getListener()
+                    : listener);
+        }
+
+        public void makeCall(SipProfile peerProfile,
+                SessionDescription sessionDescription) {
+            try {
+                processCommand(
+                        new MakeCallCommand(peerProfile, sessionDescription));
+            } catch (SipException e) {
+                onError(e);
+            }
+        }
+
+        public void answerCall(SessionDescription sessionDescription) {
+            try {
+                processCommand(
+                        new MakeCallCommand(mPeerProfile, sessionDescription));
+            } catch (SipException e) {
+                onError(e);
+            }
+        }
+
+        public void endCall() {
+            try {
+                processCommand(END_CALL);
+            } catch (SipException e) {
+                onError(e);
+            }
+        }
+
+        public void changeCall(SessionDescription sessionDescription) {
+            try {
+                processCommand(
+                        new MakeCallCommand(mPeerProfile, sessionDescription));
+            } catch (SipException e) {
+                onError(e);
+            }
+        }
+
+        public void register(int duration) {
+            try {
+                processCommand(new RegisterCommand(duration));
+            } catch (SipException e) {
+                onRegistrationFailed(e);
+            }
+        }
+
+        public void unregister() {
+            try {
+                processCommand(DEREGISTER);
+            } catch (SipException e) {
+                onRegistrationFailed(e);
+            }
+        }
+
+        public boolean isReRegisterRequired() {
+            return mReRegisterFlag;
+        }
+
+        public void clearReRegisterRequired() {
+            mReRegisterFlag = false;
+        }
+
+        public void sendKeepAlive() {
+            mState = SipSessionState.PINGING;
+            try {
+                processCommand(new OptionsCommand());
+                while (SipSessionState.PINGING.equals(mState)) {
+                    Thread.sleep(1000);
+                }
+            } catch (SipException e) {
+                Log.e(TAG, "sendKeepAlive failed", e);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "sendKeepAlive interrupted", e);
+            }
+        }
+
+        private void processCommand(EventObject command) throws SipException {
+            if (!process(command)) {
+                throw new SipException("wrong state to execute: " + command);
+            }
+        }
+
+        protected String generateTag() {
+            // 32-bit randomness
+            return String.valueOf((long) (Math.random() * 0x100000000L));
+        }
+
+        public String toString() {
+            try {
+                String s = super.toString();
+                return s.substring(s.indexOf("@")) + ":" + mState;
+            } catch (Throwable e) {
+                return super.toString();
+            }
+        }
+
+        public boolean process(EventObject evt) throws SipException {
+            Log.d(TAG, " ~~~~~   " + this + ": " + mState + ": processing "
+                    + log(evt));
+            synchronized (SipSessionGroup.this) {
+                if (isClosed()) return false;
+
+                Dialog dialog = null;
+                if (evt instanceof RequestEvent) {
+                    dialog = ((RequestEvent) evt).getDialog();
+                } else if (evt instanceof ResponseEvent) {
+                    dialog = ((ResponseEvent) evt).getDialog();
+                }
+                if (dialog != null) mDialog = dialog;
+
+                boolean processed;
+
+                switch (mState) {
+                case REGISTERING:
+                case DEREGISTERING:
+                    processed = registeringToReady(evt);
+                    break;
+                case PINGING:
+                    processed = keepAliveProcess(evt);
+                    break;
+                case READY_TO_CALL:
+                    processed = readyForCall(evt);
+                    break;
+                case INCOMING_CALL:
+                    processed = incomingCall(evt);
+                    break;
+                case INCOMING_CALL_ANSWERING:
+                    processed = incomingCallToInCall(evt);
+                    break;
+                case OUTGOING_CALL:
+                case OUTGOING_CALL_RING_BACK:
+                    processed = outgoingCall(evt);
+                    break;
+                case OUTGOING_CALL_CANCELING:
+                    processed = outgoingCallToReady(evt);
+                    break;
+                case IN_CALL:
+                    processed = inCall(evt);
+                    break;
+                default:
+                    processed = false;
+                }
+                return (processed || processExceptions(evt));
+            }
+        }
+
+        private boolean processExceptions(EventObject evt) throws SipException {
+            if (isRequestEvent(Request.BYE, evt)) {
+                // terminate the call whenever a BYE is received
+                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
+                endCallNormally();
+                return true;
+            } else if (isRequestEvent(Request.CANCEL, evt)) {
+                mSipHelper.sendResponse((RequestEvent) evt,
+                        Response.CALL_OR_TRANSACTION_DOES_NOT_EXIST);
+                return true;
+            } else if (evt instanceof TransactionTerminatedEvent) {
+                if (evt instanceof TimeoutEvent) {
+                    processTimeout((TimeoutEvent) evt);
+                } else {
+                    Log.d(TAG, "Transaction terminated:" + this);
+                    if (!SipSessionState.IN_CALL.equals(mState)) {
+                        removeSipSession(this);
+                    }
+                    return true;
+                }
+                return true;
+            } else if (evt instanceof DialogTerminatedEvent) {
+                processDialogTerminated((DialogTerminatedEvent) evt);
+                return true;
+            }
+            return false;
+        }
+
+        private void processDialogTerminated(DialogTerminatedEvent event) {
+            if (mDialog == event.getDialog()) {
+                onError(new SipException("dialog terminated"));
+            } else {
+                Log.d(TAG, "not the current dialog; current=" + mDialog
+                        + ", terminated=" + event.getDialog());
+            }
+        }
+
+        private void processTimeout(TimeoutEvent event) {
+            Log.d(TAG, "processing Timeout..." + event);
+            Transaction current = event.isServerTransaction()
+                    ? mServerTransaction
+                    : mClientTransaction;
+            Transaction target = event.isServerTransaction()
+                    ? event.getServerTransaction()
+                    : event.getClientTransaction();
+
+            if ((current != target) && (mState != SipSessionState.PINGING)) {
+                Log.d(TAG, "not the current transaction; current=" + current
+                        + ", timed out=" + target);
+                return;
+            }
+            switch (mState) {
+            case REGISTERING:
+            case DEREGISTERING:
+                reset();
+                mProxy.onRegistrationTimeout(this);
+                break;
+            case INCOMING_CALL:
+            case INCOMING_CALL_ANSWERING:
+            case OUTGOING_CALL_CANCELING:
+                endCallOnError(new SipException("timed out"));
+                break;
+            case PINGING:
+                reset();
+                mReRegisterFlag = true;
+                mState = SipSessionState.READY_TO_CALL;
+                break;
+
+            default:
+                // do nothing
+                break;
+            }
+        }
+
+        private int getExpiryTime(Response response) {
+            int expires = EXPIRY_TIME;
+            ExpiresHeader expiresHeader = (ExpiresHeader)
+                    response.getHeader(ExpiresHeader.NAME);
+            if (expiresHeader != null) expires = expiresHeader.getExpires();
+            expiresHeader = (ExpiresHeader)
+                    response.getHeader(MinExpiresHeader.NAME);
+            if (expiresHeader != null) {
+                expires = Math.max(expires, expiresHeader.getExpires());
+            }
+            return expires;
+        }
+
+        private boolean keepAliveProcess(EventObject evt) throws SipException {
+            if (evt instanceof OptionsCommand) {
+                mClientTransaction = mSipHelper.sendKeepAlive(mLocalProfile,
+                        generateTag());
+                mDialog = mClientTransaction.getDialog();
+                addSipSession(this);
+                return true;
+            } else if (evt instanceof ResponseEvent) {
+                return parseOptionsResult(evt);
+            }
+            return false;
+        }
+
+        private boolean parseOptionsResult(EventObject evt) {
+            if (expectResponse(Request.OPTIONS, evt)) {
+                ResponseEvent event = (ResponseEvent) evt;
+                int rPort = getRPortFromResponse(event.getResponse());
+                if (rPort != -1) {
+                    if (mRPort == 0) mRPort = rPort;
+                    if (mRPort != rPort) {
+                        mReRegisterFlag = true;
+                        Log.w(TAG, String.format("rport is changed: %d <> %d",
+                                mRPort, rPort));
+                        mRPort = rPort;
+                    } else {
+                        Log.w(TAG, "rport is the same: " + rPort);
+                    }
+                } else {
+                    Log.w(TAG, "peer did not respect our rport request");
+                }
+                mState = SipSessionState.READY_TO_CALL;
+                return true;
+            }
+            return false;
+        }
+
+        private int getRPortFromResponse(Response response) {
+            ViaHeader viaHeader = (ViaHeader)(response.getHeader(
+                    SIPHeaderNames.VIA));
+            return (viaHeader == null) ? -1 : viaHeader.getRPort();
+        }
+
+        private boolean registeringToReady(EventObject evt)
+                throws SipException {
+            if (expectResponse(Request.REGISTER, evt)) {
+                ResponseEvent event = (ResponseEvent) evt;
+                Response response = event.getResponse();
+
+                int statusCode = response.getStatusCode();
+                switch (statusCode) {
+                case Response.OK:
+                    SipSessionState state = mState;
+                    reset();
+                    onRegistrationDone((state == SipSessionState.REGISTERING)
+                            ? getExpiryTime(((ResponseEvent) evt).getResponse())
+                            : -1);
+                    mLastNonce = null;
+                    mRPort = 0;
+                    return true;
+                case Response.UNAUTHORIZED:
+                case Response.PROXY_AUTHENTICATION_REQUIRED:
+                    String nonce = getNonceFromResponse(response);
+                    if (((nonce != null) && nonce.equals(mLastNonce)) ||
+                            (nonce == mLastNonce)) {
+                        Log.v(TAG, "Incorrect username/password");
+                        reset();
+                        onRegistrationFailed(createCallbackException(response));
+                    } else {
+                        mSipHelper.handleChallenge(event, getAccountManager());
+                        mLastNonce = nonce;
+                    }
+                    return true;
+                default:
+                    if (statusCode >= 500) {
+                        reset();
+                        onRegistrationFailed(createCallbackException(response));
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        private AccountManager getAccountManager() {
+            return new AccountManager() {
+                public UserCredentials getCredentials(ClientTransaction
+                        challengedTransaction, String realm) {
+                    return new UserCredentials() {
+                        public String getUserName() {
+                            return mLocalProfile.getUserName();
+                        }
+
+                        public String getPassword() {
+                            return mPassword;
+                        }
+
+                        public String getSipDomain() {
+                            return mLocalProfile.getSipDomain();
+                        }
+                    };
+                }
+            };
+        }
+
+        private String getNonceFromResponse(Response response) {
+            WWWAuthenticate authHeader = (WWWAuthenticate)(response.getHeader(
+                    SIPHeaderNames.WWW_AUTHENTICATE));
+            return (authHeader == null) ? null : authHeader.getNonce();
+        }
+
+        private boolean readyForCall(EventObject evt) throws SipException {
+            // expect MakeCallCommand, RegisterCommand, DEREGISTER
+            if (evt instanceof MakeCallCommand) {
+                MakeCallCommand cmd = (MakeCallCommand) evt;
+                mPeerProfile = cmd.getPeerProfile();
+                SessionDescription sessionDescription =
+                        cmd.getSessionDescription();
+                mClientTransaction = mSipHelper.sendInvite(mLocalProfile,
+                        mPeerProfile, sessionDescription, generateTag());
+                mDialog = mClientTransaction.getDialog();
+                addSipSession(this);
+                mState = SipSessionState.OUTGOING_CALL;
+                mProxy.onCalling(this);
+                return true;
+            } else if (evt instanceof RegisterCommand) {
+                int duration = ((RegisterCommand) evt).getDuration();
+                mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
+                        generateTag(), duration);
+                mDialog = mClientTransaction.getDialog();
+                addSipSession(this);
+                mState = SipSessionState.REGISTERING;
+                mProxy.onRegistering(this);
+                return true;
+            } else if (DEREGISTER == evt) {
+                mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
+                        generateTag(), 0);
+                mDialog = mClientTransaction.getDialog();
+                addSipSession(this);
+                mState = SipSessionState.DEREGISTERING;
+                mProxy.onRegistering(this);
+                return true;
+            }
+            return false;
+        }
+
+        private boolean incomingCall(EventObject evt) throws SipException {
+            // expect MakeCallCommand(answering) , END_CALL cmd , Cancel
+            if (evt instanceof MakeCallCommand) {
+                // answer call
+                mServerTransaction = mSipHelper.sendInviteOk(mInviteReceived,
+                        mLocalProfile,
+                        ((MakeCallCommand) evt).getSessionDescription(),
+                        mServerTransaction);
+                mState = SipSessionState.INCOMING_CALL_ANSWERING;
+                return true;
+            } else if (END_CALL == evt) {
+                mSipHelper.sendInviteBusyHere(mInviteReceived,
+                        mServerTransaction);
+                endCallNormally();
+                return true;
+            } else if (isRequestEvent(Request.CANCEL, evt)) {
+                RequestEvent event = (RequestEvent) evt;
+                mSipHelper.sendResponse(event, Response.OK);
+                mSipHelper.sendInviteRequestTerminated(
+                        mInviteReceived.getRequest(), mServerTransaction);
+                endCallNormally();
+                return true;
+            }
+            return false;
+        }
+
+        private boolean incomingCallToInCall(EventObject evt)
+                throws SipException {
+            // expect ACK, CANCEL request
+            if (isRequestEvent(Request.ACK, evt)) {
+                establishCall();
+                return true;
+            } else if (isRequestEvent(Request.CANCEL, evt)) {
+                // http://tools.ietf.org/html/rfc3261#section-9.2
+                // Final response has been sent; do nothing here.
+                return true;
+            }
+            return false;
+        }
+
+        private boolean outgoingCall(EventObject evt) throws SipException {
+            if (expectResponse(Request.INVITE, evt)) {
+                ResponseEvent event = (ResponseEvent) evt;
+                Response response = event.getResponse();
+
+                int statusCode = response.getStatusCode();
+                switch (statusCode) {
+                case Response.RINGING:
+                    if (mState == SipSessionState.OUTGOING_CALL) {
+                        mState = SipSessionState.OUTGOING_CALL_RING_BACK;
+                        mProxy.onRingingBack(this);
+                    }
+                    return true;
+                case Response.OK:
+                    mSipHelper.sendInviteAck(event, mDialog);
+                    mPeerSessionDescription = response.getRawContent();
+                    establishCall();
+                    return true;
+                case Response.PROXY_AUTHENTICATION_REQUIRED:
+                    mClientTransaction = mSipHelper.handleChallenge(
+                            (ResponseEvent) evt, getAccountManager());
+                    mDialog = mClientTransaction.getDialog();
+                    addSipSession(this);
+                    return true;
+                case Response.BUSY_HERE:
+                    reset();
+                    mProxy.onCallBusy(this);
+                    return true;
+                case Response.REQUEST_PENDING:
+                    // TODO:
+                    // rfc3261#section-14.1; re-schedule invite
+                    return true;
+                default:
+                    if (statusCode >= 400) {
+                        // error: an ack is sent automatically by the stack
+                        onError(createCallbackException(response));
+                        return true;
+                    } else if (statusCode >= 300) {
+                        // TODO: handle 3xx (redirect)
+                    } else {
+                        return true;
+                    }
+                }
+                return false;
+            } else if (END_CALL == evt) {
+                // RFC says that UA should not send out cancel when no
+                // response comes back yet. We are cheating for not checking
+                // response.
+                mSipHelper.sendCancel(mClientTransaction);
+                mState = SipSessionState.OUTGOING_CALL_CANCELING;
+                return true;
+            }
+            return false;
+        }
+
+        private boolean outgoingCallToReady(EventObject evt)
+                throws SipException {
+            if (evt instanceof ResponseEvent) {
+                ResponseEvent event = (ResponseEvent) evt;
+                Response response = event.getResponse();
+                int statusCode = response.getStatusCode();
+                if (expectResponse(Request.CANCEL, evt)) {
+                    if (statusCode == Response.OK) {
+                        // do nothing; wait for REQUEST_TERMINATED
+                        return true;
+                    }
+                } else if (expectResponse(Request.INVITE, evt)) {
+                    if (statusCode == Response.OK) {
+                        outgoingCall(evt); // abort Cancel
+                        return true;
+                    }
+                } else {
+                    return false;
+                }
+
+                if (statusCode >= 400) {
+                    onError(createCallbackException(response));
+                    return true;
+                }
+            } else if (evt instanceof TransactionTerminatedEvent) {
+                // rfc3261#section-14.1:
+                // if re-invite gets timed out, terminate the dialog; but
+                // re-invite is not reliable, just let it go and pretend
+                // nothing happened.
+                onError(new SipException("timed out"));
+            }
+            return false;
+        }
+
+        private boolean inCall(EventObject evt) throws SipException {
+            // expect END_CALL cmd, BYE request, hold call (MakeCallCommand)
+            // OK retransmission is handled in SipStack
+            if (END_CALL == evt) {
+                // rfc3261#section-15.1.1
+                mSipHelper.sendBye(mDialog);
+                endCallNormally();
+                return true;
+            } else if (isRequestEvent(Request.INVITE, evt)) {
+                // got Re-INVITE
+                RequestEvent event = mInviteReceived = (RequestEvent) evt;
+                mState = SipSessionState.INCOMING_CALL;
+                mPeerSessionDescription = event.getRequest().getRawContent();
+                mServerTransaction = null;
+                mProxy.onRinging(this, mPeerProfile, mPeerSessionDescription);
+                return true;
+            } else if (isRequestEvent(Request.BYE, evt)) {
+                mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
+                endCallNormally();
+                return true;
+            } else if (evt instanceof MakeCallCommand) {
+                // to change call
+                mClientTransaction = mSipHelper.sendReinvite(mDialog,
+                        ((MakeCallCommand) evt).getSessionDescription());
+                mState = SipSessionState.OUTGOING_CALL;
+                return true;
+            }
+            return false;
+        }
+
+        private Exception createCallbackException(Response response) {
+            return new SipException(String.format("Response: %s (%d)",
+                    response.getReasonPhrase(), response.getStatusCode()));
+        }
+
+        private void establishCall() {
+            mState = SipSessionState.IN_CALL;
+            mInCall = true;
+            mProxy.onCallEstablished(this, mPeerSessionDescription);
+        }
+
+        private void fallbackToPreviousInCall(Throwable exception) {
+            mState = SipSessionState.IN_CALL;
+            mProxy.onCallChangeFailed(this, exception.getClass().getName(),
+                    exception.getMessage());
+        }
+
+        private void endCallNormally() {
+            reset();
+            mProxy.onCallEnded(this);
+        }
+
+        private void endCallOnError(Throwable exception) {
+            reset();
+            mProxy.onError(this, exception.getClass().getName(),
+                    exception.getMessage());
+        }
+
+        private void onError(Throwable exception) {
+            if (mInCall) {
+                fallbackToPreviousInCall(exception);
+            } else {
+                endCallOnError(exception);
+            }
+        }
+
+        private void onRegistrationDone(int duration) {
+            mProxy.onRegistrationDone(this, duration);
+        }
+
+        private void onRegistrationFailed(Throwable exception) {
+            mProxy.onRegistrationFailed(this, exception.getClass().getName(),
+                    exception.getMessage());
+        }
+    }
+
+    /**
+     * @return true if the event is a request event matching the specified
+     *      method; false otherwise
+     */
+    private static boolean isRequestEvent(String method, EventObject event) {
+        try {
+            if (event instanceof RequestEvent) {
+                RequestEvent requestEvent = (RequestEvent) event;
+                return method.equals(requestEvent.getRequest().getMethod());
+            }
+        } catch (Throwable e) {
+        }
+        return false;
+    }
+
+    private static String getCseqMethod(Message message) {
+        return ((CSeqHeader) message.getHeader(CSeqHeader.NAME)).getMethod();
+    }
+
+    /**
+     * @return true if the event is a response event and the CSeqHeader method
+     * match the given arguments; false otherwise
+     */
+    private static boolean expectResponse(
+            String expectedMethod, EventObject evt) {
+        if (evt instanceof ResponseEvent) {
+            ResponseEvent event = (ResponseEvent) evt;
+            Response response = event.getResponse();
+            return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the event is a response event and the response code and
+     *      CSeqHeader method match the given arguments; false otherwise
+     */
+    private static boolean expectResponse(
+            int responseCode, String expectedMethod, EventObject evt) {
+        if (evt instanceof ResponseEvent) {
+            ResponseEvent event = (ResponseEvent) evt;
+            Response response = event.getResponse();
+            if (response.getStatusCode() == responseCode) {
+                return expectedMethod.equalsIgnoreCase(getCseqMethod(response));
+            }
+        }
+        return false;
+    }
+
+    private static SipProfile createPeerProfile(Request request)
+            throws SipException {
+        try {
+            FromHeader fromHeader =
+                    (FromHeader) request.getHeader(FromHeader.NAME);
+            Address address = fromHeader.getAddress();
+            SipURI uri = (SipURI) address.getURI();
+            String username = uri.getUser();
+            if (username == null) username = ANONYMOUS;
+            return new SipProfile.Builder(username, uri.getHost())
+                    .setPort(uri.getPort())
+                    .setDisplayName(address.getDisplayName())
+                    .build();
+        } catch (InvalidArgumentException e) {
+            throw new SipException("createPeerProfile()", e);
+        } catch (ParseException e) {
+            throw new SipException("createPeerProfile()", e);
+        }
+    }
+
+    private static String log(EventObject evt) {
+        if (evt instanceof RequestEvent) {
+            return ((RequestEvent) evt).getRequest().toString();
+        } else if (evt instanceof ResponseEvent) {
+            return ((ResponseEvent) evt).getResponse().toString();
+        } else {
+            return evt.toString();
+        }
+    }
+
+    private class OptionsCommand extends EventObject {
+        public OptionsCommand() {
+            super(SipSessionGroup.this);
+        }
+    }
+
+    private class RegisterCommand extends EventObject {
+        private int mDuration;
+
+        public RegisterCommand(int duration) {
+            super(SipSessionGroup.this);
+            mDuration = duration;
+        }
+
+        public int getDuration() {
+            return mDuration;
+        }
+    }
+
+    private class MakeCallCommand extends EventObject {
+        private SessionDescription mSessionDescription;
+
+        public MakeCallCommand(SipProfile peerProfile,
+                SessionDescription sessionDescription) {
+            super(peerProfile);
+            mSessionDescription = sessionDescription;
+        }
+
+        public SipProfile getPeerProfile() {
+            return (SipProfile) getSource();
+        }
+
+        public SessionDescription getSessionDescription() {
+            return mSessionDescription;
+        }
+    }
+
+}
diff --git a/services/java/com/android/server/sip/SipSessionListenerProxy.java b/services/java/com/android/server/sip/SipSessionListenerProxy.java
new file mode 100644
index 0000000..fd49fd8
--- /dev/null
+++ b/services/java/com/android/server/sip/SipSessionListenerProxy.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.sip;
+
+import android.net.sip.ISipSession;
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SipProfile;
+import android.util.Log;
+
+/** Class to help safely run a callback in a different thread. */
+class SipSessionListenerProxy extends ISipSessionListener.Stub {
+    private static final String TAG = "SipSession";
+
+    private ISipSessionListener mListener;
+
+    public void setListener(ISipSessionListener listener) {
+        mListener = listener;
+    }
+
+    public ISipSessionListener getListener() {
+        return mListener;
+    }
+
+    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).start();
+    }
+
+    public void onCalling(final ISipSession session) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onCalling(session);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onCalling()", t);
+                }
+            }
+        });
+    }
+
+    public void onRinging(final ISipSession session, final SipProfile caller,
+            final byte[] sessionDescription) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onRinging(session, caller, sessionDescription);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onRinging()", t);
+                }
+            }
+        });
+    }
+
+    public void onRingingBack(final ISipSession session) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onRingingBack(session);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onRingingBack()", t);
+                }
+            }
+        });
+    }
+
+    public void onCallEstablished(final ISipSession session,
+            final byte[] sessionDescription) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onCallEstablished(session, sessionDescription);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onCallEstablished()", t);
+                }
+            }
+        });
+    }
+
+    public void onCallEnded(final ISipSession session) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onCallEnded(session);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onCallEnded()", t);
+                }
+            }
+        });
+    }
+
+    public void onCallBusy(final ISipSession session) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onCallBusy(session);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onCallBusy()", t);
+                }
+            }
+        });
+    }
+
+    public void onCallChangeFailed(final ISipSession session,
+            final String className, final String message) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onCallChangeFailed(session, className, message);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onCallChangeFailed()", t);
+                }
+            }
+        });
+    }
+
+    public void onError(final ISipSession session, final String className,
+            final String message) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onError(session, className, message);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onError()", t);
+                }
+            }
+        });
+    }
+
+    public void onRegistering(final ISipSession session) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onRegistering(session);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onRegistering()", t);
+                }
+            }
+        });
+    }
+
+    public void onRegistrationDone(final ISipSession session,
+            final int duration) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onRegistrationDone(session, duration);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onRegistrationDone()", t);
+                }
+            }
+        });
+    }
+
+    public void onRegistrationFailed(final ISipSession session,
+            final String className, final String message) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onRegistrationFailed(session, className, message);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onRegistrationFailed()", t);
+                }
+            }
+        });
+    }
+
+    public void onRegistrationTimeout(final ISipSession session) {
+        if (mListener == null) return;
+        proxy(new Runnable() {
+            public void run() {
+                try {
+                    mListener.onRegistrationTimeout(session);
+                } catch (Throwable t) {
+                    Log.w(TAG, "onRegistrationTimeout()", t);
+                }
+            }
+        });
+    }
+}