summaryrefslogtreecommitdiffstats
path: root/voip
diff options
context:
space:
mode:
authorHung-ying Tyan <tyanh@google.com>2010-09-25 22:49:59 +0800
committerHung-ying Tyan <tyanh@google.com>2010-09-28 05:19:35 +0800
commit7e54ef71db3320a751571bba5259fba816399421 (patch)
treeac4c1a7d45c1406366e7f7ba02ec0b0d6117d386 /voip
parent04a426f5b50a628fb73bc29b003b0e5bece17c27 (diff)
downloadframeworks_base-7e54ef71db3320a751571bba5259fba816399421.zip
frameworks_base-7e54ef71db3320a751571bba5259fba816399421.tar.gz
frameworks_base-7e54ef71db3320a751571bba5259fba816399421.tar.bz2
Move SipService out of SystemServer to phone process.
Companion CL: https://android-git/g/#change,70187 http://b/issue?id=2998069 Change-Id: I90923ac522ef363a4e04292f652d413c5a1526ad
Diffstat (limited to 'voip')
-rw-r--r--voip/java/com/android/server/sip/SipHelper.java464
-rw-r--r--voip/java/com/android/server/sip/SipService.java1234
-rw-r--r--voip/java/com/android/server/sip/SipSessionGroup.java1393
-rw-r--r--voip/java/com/android/server/sip/SipSessionListenerProxy.java217
4 files changed, 3308 insertions, 0 deletions
diff --git a/voip/java/com/android/server/sip/SipHelper.java b/voip/java/com/android/server/sip/SipHelper.java
new file mode 100644
index 0000000..050eddc
--- /dev/null
+++ b/voip/java/com/android/server/sip/SipHelper.java
@@ -0,0 +1,464 @@
+/*
+ * 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.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 static final boolean DEBUG = true;
+
+ 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,
+ String sessionDescription, String tag)
+ throws SipException {
+ try {
+ FromHeader fromHeader = createFromHeader(caller, tag);
+ ToHeader toHeader = createToHeader(callee);
+ SipURI requestURI = callee.getUri();
+ List<ViaHeader> viaHeaders = createViaHeaders();
+ CallIdHeader callIdHeader = createCallIdHeader();
+ CSeqHeader cSeqHeader = createCSeqHeader(Request.INVITE);
+ MaxForwardsHeader maxForwards = createMaxForwardsHeader();
+
+ Request request = mMessageFactory.createRequest(requestURI,
+ Request.INVITE, callIdHeader, cSeqHeader, fromHeader,
+ toHeader, viaHeaders, maxForwards);
+
+ request.addHeader(createContactHeader(caller));
+ request.setContent(sessionDescription,
+ mHeaderFactory.createContentTypeHeader(
+ "application", "sdp"));
+
+ ClientTransaction clientTransaction =
+ mSipProvider.getNewClientTransaction(request);
+ if (DEBUG) Log.d(TAG, "send INVITE: " + request);
+ clientTransaction.sendRequest();
+ return clientTransaction;
+ } catch (ParseException e) {
+ throw new SipException("sendInvite()", e);
+ }
+ }
+
+ public ClientTransaction sendReinvite(Dialog dialog,
+ String sessionDescription) throws SipException {
+ try {
+ Request request = dialog.createRequest(Request.INVITE);
+ request.setContent(sessionDescription,
+ mHeaderFactory.createContentTypeHeader(
+ "application", "sdp"));
+
+ ClientTransaction clientTransaction =
+ mSipProvider.getNewClientTransaction(request);
+ if (DEBUG) Log.d(TAG, "send RE-INVITE: " + 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);
+ if (DEBUG) Log.d(TAG, "send RINGING: " + response);
+ 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, String sessionDescription,
+ ServerTransaction inviteTransaction)
+ throws SipException {
+ try {
+ Request request = event.getRequest();
+ Response response = mMessageFactory.createResponse(Response.OK,
+ request);
+ response.addHeader(createContactHeader(localProfile));
+ response.setContent(sessionDescription,
+ mHeaderFactory.createContentTypeHeader(
+ "application", "sdp"));
+
+ if (inviteTransaction == null) {
+ inviteTransaction = getServerTransaction(event);
+ }
+
+ if (inviteTransaction.getState() != TransactionState.COMPLETED) {
+ if (DEBUG) Log.d(TAG, "send OK: " + response);
+ 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) {
+ if (DEBUG) Log.d(TAG, "send BUSY HERE: " + response);
+ 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();
+ Request ack = dialog.createAck(cseq);
+ if (DEBUG) Log.d(TAG, "send ACK: " + ack);
+ dialog.sendAck(ack);
+ }
+
+ public void sendBye(Dialog dialog) throws SipException {
+ Request byeRequest = dialog.createRequest(Request.BYE);
+ if (DEBUG) Log.d(TAG, "send BYE: " + byeRequest);
+ dialog.sendRequest(mSipProvider.getNewClientTransaction(byeRequest));
+ }
+
+ public void sendCancel(ClientTransaction inviteTransaction)
+ throws SipException {
+ Request cancelRequest = inviteTransaction.createCancel();
+ if (DEBUG) Log.d(TAG, "send CANCEL: " + cancelRequest);
+ mSipProvider.getNewClientTransaction(cancelRequest).sendRequest();
+ }
+
+ public void sendResponse(RequestEvent event, int responseCode)
+ throws SipException {
+ try {
+ Response response = mMessageFactory.createResponse(
+ responseCode, event.getRequest());
+ if (DEBUG) Log.d(TAG, "send response: " + response);
+ getServerTransaction(event).sendResponse(response);
+ } catch (ParseException e) {
+ throw new SipException("sendResponse()", e);
+ }
+ }
+
+ public void sendInviteRequestTerminated(Request inviteRequest,
+ ServerTransaction inviteTransaction) throws SipException {
+ try {
+ Response response = mMessageFactory.createResponse(
+ Response.REQUEST_TERMINATED, inviteRequest);
+ if (DEBUG) Log.d(TAG, "send response: " + response);
+ inviteTransaction.sendResponse(response);
+ } 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/voip/java/com/android/server/sip/SipService.java b/voip/java/com/android/server/sip/SipService.java
new file mode 100644
index 0000000..0ff5586
--- /dev/null
+++ b/voip/java/com/android/server/sip/SipService.java
@@ -0,0 +1,1234 @@
+/*
+ * 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.SipErrorCode;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.net.sip.SipSession;
+import android.net.sip.SipSessionAdapter;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+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;
+
+/**
+ * @hide
+ */
+public final class SipService extends ISipService.Stub {
+ private static final String TAG = "SipService";
+ private static final boolean DEBUG = true;
+ private static final boolean DEBUG_TIMER = DEBUG && false;
+ 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;
+ private boolean mWifiOnly;
+
+ private MyExecutor mExecutor;
+
+ // 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;
+
+ /**
+ * Starts the SIP service. Do nothing if the SIP API is not supported on the
+ * device.
+ */
+ public static void start(Context context) {
+ if (SipManager.isApiSupported(context)) {
+ ServiceManager.addService("sip", new SipService(context));
+ Log.i(TAG, "SIP service started");
+ }
+ }
+
+ private SipService(Context context) {
+ if (DEBUG) Log.d(TAG, " service started!");
+ mContext = context;
+ mConnectivityReceiver = new ConnectivityReceiver();
+ context.registerReceiver(mConnectivityReceiver,
+ new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+
+ mTimer = new WakeupTimer(context);
+ mWifiOnly = SipManager.isSipWifiOnly(context);
+ }
+
+ private MyExecutor getExecutor() {
+ // create mExecutor lazily
+ if (mExecutor == null) mExecutor = new MyExecutor();
+ return mExecutor;
+ }
+
+ 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) {
+ localProfile.setCallingUid(Binder.getCallingUid());
+ 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.ACTION_SIP_INCOMING_CALL, null);
+ }
+
+ public synchronized void open3(SipProfile localProfile,
+ String incomingCallBroadcastAction, ISipSessionListener listener) {
+ localProfile.setCallingUid(Binder.getCallingUid());
+ if (TextUtils.isEmpty(incomingCallBroadcastAction)) {
+ throw new RuntimeException(
+ "empty broadcast action for incoming call");
+ }
+ if (DEBUG) Log.d(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.close();
+ 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) {
+ localProfile.setCallingUid(Binder.getCallingUid());
+ 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) {
+ if (DEBUG) Log.d(TAG, "notify: profile added: " + localProfile);
+ Intent intent = new Intent(SipManager.ACTION_SIP_ADD_PHONE);
+ intent.putExtra(SipManager.EXTRA_LOCAL_URI, localProfile.getUriString());
+ mContext.sendBroadcast(intent);
+ }
+
+ private void notifyProfileRemoved(SipProfile localProfile) {
+ if (DEBUG) Log.d(TAG, "notify: profile removed: " + localProfile);
+ Intent intent = new Intent(SipManager.ACTION_SIP_REMOVE_PHONE);
+ intent.putExtra(SipManager.EXTRA_LOCAL_URI, 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) {
+ if (DEBUG) Log.d(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) {
+ if (DEBUG) Log.d(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) {
+ if (DEBUG) Log.d(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).setPassword("*").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);
+ }
+ if (DEBUG) Log.d(TAG, " openToReceiveCalls: " + getUri() + ": "
+ + mIncomingCallBroadcastAction);
+ }
+
+ public void onConnectivityChanged(boolean connected)
+ throws SipException {
+ mSipGroup.onConnectivityChanged();
+ if (connected) {
+ resetGroup(mLocalIp);
+ if (mOpened) openToReceiveCalls();
+ } else {
+ // close mSipGroup but remember mOpened
+ if (DEBUG) Log.d(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 close() {
+ mOpened = false;
+ mSipGroup.close();
+ mAutoRegistration.stop();
+ if (DEBUG) Log.d(TAG, " close: " + getUri() + ": "
+ + mIncomingCallBroadcastAction);
+ }
+
+ public ISipSession createSession(ISipSessionListener listener) {
+ return mSipGroup.createSession(listener);
+ }
+
+ @Override
+ public void onRinging(ISipSession session, SipProfile caller,
+ String sessionDescription) {
+ synchronized (SipService.this) {
+ try {
+ if (!isRegistered()) {
+ session.endCall();
+ return;
+ }
+
+ // send out incoming call broadcast
+ addPendingSession(session);
+ Intent intent = SipManager.createIncomingCallBroadcast(
+ session.getCallId(), sessionDescription)
+ .setAction(mIncomingCallBroadcastAction);
+ if (DEBUG) Log.d(TAG, " ringing~~ " + getUri() + ": "
+ + caller.getUri() + ": " + session.getCallId()
+ + " " + mIncomingCallBroadcastAction);
+ mContext.sendBroadcast(intent);
+ } catch (RemoteException e) {
+ // should never happen with a local call
+ Log.e(TAG, "processCall()", e);
+ }
+ }
+ }
+
+ @Override
+ public void onError(ISipSession session, int errorCode,
+ String message) {
+ if (DEBUG) Log.d(TAG, "sip session error: "
+ + SipErrorCode.toString(errorCode) + ": " + 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 = 10;
+ private SipSessionGroup.SipSessionImpl mSession;
+
+ public KeepAliveProcess(SipSessionGroup.SipSessionImpl session) {
+ mSession = session;
+ }
+
+ public void start() {
+ mTimer.set(INTERVAL * 1000, this);
+ }
+
+ public void run() {
+ // delegate to mExecutor
+ getExecutor().addTask(new Runnable() {
+ public void run() {
+ realRun();
+ }
+ });
+ }
+
+ private void realRun() {
+ synchronized (SipService.this) {
+ SipSessionGroup.SipSessionImpl session = mSession.duplicate();
+ if (DEBUG) 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 int mErrorCode;
+ private String mErrorMessage;
+
+ 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();
+ if (DEBUG) Log.d(TAG, "start AutoRegistrationProcess for "
+ + mSession.getLocalProfile().getUriString());
+ }
+ }
+
+ public void stop() {
+ stop(false);
+ }
+
+ private void stopButKeepStates() {
+ stop(true);
+ }
+
+ private void stop(boolean keepStates) {
+ if (mSession == null) return;
+ if (mConnected && mRegistered) mSession.unregister();
+ mTimer.cancel(this);
+ if (mKeepAliveProcess != null) {
+ mKeepAliveProcess.stop();
+ mKeepAliveProcess = null;
+ }
+ if (!keepStates) {
+ mSession = null;
+ mRegistered = false;
+ }
+ }
+
+ private boolean isStopped() {
+ return (mSession == null);
+ }
+
+ public void setListener(ISipSessionListener listener) {
+ synchronized (SipService.this) {
+ mProxy.setListener(listener);
+ if (mSession == null) return;
+
+ try {
+ int state = (mSession == null)
+ ? SipSession.State.READY_TO_CALL
+ : mSession.getState();
+ if ((state == SipSession.State.REGISTERING)
+ || (state == SipSession.State.DEREGISTERING)) {
+ mProxy.onRegistering(mSession);
+ } else if (mRegistered) {
+ int duration = (int)
+ (mExpiryTime - SystemClock.elapsedRealtime());
+ mProxy.onRegistrationDone(mSession, duration);
+ } else if (mErrorCode != SipErrorCode.NO_ERROR) {
+ if (mErrorCode == SipErrorCode.TIME_OUT) {
+ mProxy.onRegistrationTimeout(mSession);
+ } else {
+ mProxy.onRegistrationFailed(mSession, mErrorCode,
+ mErrorMessage);
+ }
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, "setListener(): " + t);
+ }
+ }
+ }
+
+ public boolean isRegistered() {
+ return mRegistered;
+ }
+
+ public void run() {
+ // delegate to mExecutor
+ getExecutor().addTask(new Runnable() {
+ public void run() {
+ realRun();
+ }
+ });
+ }
+
+ private void realRun() {
+ mErrorCode = SipErrorCode.NO_ERROR;
+ mErrorMessage = null;
+ if (DEBUG) Log.d(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) {
+ if (DEBUG) Log.d(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) {
+ if (DEBUG) Log.d(TAG, "onRegistering(): " + session);
+ synchronized (SipService.this) {
+ if (!isStopped() && (session != mSession)) return;
+ mRegistered = false;
+ mProxy.onRegistering(session);
+ }
+ }
+
+ @Override
+ public void onRegistrationDone(ISipSession session, int duration) {
+ if (DEBUG) Log.d(TAG, "onRegistrationDone(): " + session);
+ synchronized (SipService.this) {
+ if (!isStopped() && (session != mSession)) return;
+
+ mProxy.onRegistrationDone(session, duration);
+
+ 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;
+ if (DEBUG) Log.d(TAG, "Refresh registration immediately");
+ run();
+ }
+ }
+ }
+
+ @Override
+ public void onRegistrationFailed(ISipSession session, int errorCode,
+ String message) {
+ if (DEBUG) Log.d(TAG, "onRegistrationFailed(): " + session + ": "
+ + SipErrorCode.toString(errorCode) + ": " + message);
+ synchronized (SipService.this) {
+ if (!isStopped() && (session != mSession)) return;
+ mErrorCode = errorCode;
+ mErrorMessage = message;
+ mProxy.onRegistrationFailed(session, errorCode, message);
+
+ if (errorCode == SipErrorCode.INVALID_CREDENTIALS) {
+ if (DEBUG) Log.d(TAG, " pause auto-registration");
+ stopButKeepStates();
+ } else if (!isStopped()) {
+ onError();
+ }
+ }
+ }
+
+ @Override
+ public void onRegistrationTimeout(ISipSession session) {
+ if (DEBUG) Log.d(TAG, "onRegistrationTimeout(): " + session);
+ synchronized (SipService.this) {
+ if (!isStopped() && (session != mSession)) return;
+ mErrorCode = SipErrorCode.TIME_OUT;
+ mProxy.onRegistrationTimeout(session);
+
+ 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 (mWifiOnly && (netInfo.getType() !=
+ ConnectivityManager.TYPE_WIFI)) {
+ if (DEBUG) {
+ Log.d(TAG, "Wifi only, other connectivity ignored: "
+ + type);
+ }
+ return;
+ }
+
+ NetworkInfo activeNetInfo = getActiveNetworkInfo();
+ if (DEBUG) {
+ if (activeNetInfo != null) {
+ Log.d(TAG, "active network: "
+ + activeNetInfo.getTypeName()
+ + ((activeNetInfo.getState() == NetworkInfo.State.CONNECTED)
+ ? " CONNECTED" : " DISCONNECTED"));
+ } else {
+ Log.d(TAG, "active network: null");
+ }
+ }
+ if ((state == NetworkInfo.State.CONNECTED)
+ && (activeNetInfo != null)
+ && (activeNetInfo.getType() != netInfo.getType())) {
+ if (DEBUG) Log.d(TAG, "ignore connect event: " + type
+ + ", active: " + activeNetInfo.getTypeName());
+ return;
+ }
+
+ if (state == NetworkInfo.State.CONNECTED) {
+ if (DEBUG) Log.d(TAG, "Connectivity alert: CONNECTED " + type);
+ onChanged(type, true);
+ } else if (state == NetworkInfo.State.DISCONNECTED) {
+ if (DEBUG) Log.d(TAG, "Connectivity alert: DISCONNECTED " + type);
+ onChanged(type, false);
+ } else {
+ if (DEBUG) Log.d(TAG, "Connectivity alert not processed: "
+ + state + " " + type);
+ }
+ }
+ }
+ }
+
+ private NetworkInfo getActiveNetworkInfo() {
+ ConnectivityManager cm = (ConnectivityManager)
+ mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ return cm.getActiveNetworkInfo();
+ }
+
+ 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, 2 * 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() {
+ // delegate to mExecutor
+ getExecutor().addTask(new Runnable() {
+ public void run() {
+ realRun();
+ }
+ });
+ }
+
+ private void realRun() {
+ synchronized (SipService.this) {
+ if (mTask != this) {
+ Log.w(TAG, " unexpected task: " + mNetworkType
+ + (mConnected ? " CONNECTED" : "DISCONNECTED"));
+ return;
+ }
+ mTask = null;
+ if (DEBUG) Log.d(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;
+ if (DEBUG_TIMER) {
+ Log.d(TAG, "queue re-calculated");
+ printQueue();
+ }
+ }
+
+ // Determines the period and the trigger time of the new event and insert it
+ // to the queue.
+ private void insertEvent(MyEvent event) {
+ long now = SystemClock.elapsedRealtime();
+ if (mEventQueue.isEmpty()) {
+ event.mTriggerTime = now + event.mPeriod;
+ mEventQueue.add(event);
+ return;
+ }
+ MyEvent firstEvent = mEventQueue.first();
+ int minPeriod = firstEvent.mPeriod;
+ if (minPeriod <= event.mMaxPeriod) {
+ event.mPeriod = event.mMaxPeriod / minPeriod * minPeriod;
+ int interval = event.mMaxPeriod;
+ interval -= (int) (firstEvent.mTriggerTime - now);
+ interval = interval / minPeriod * minPeriod;
+ event.mTriggerTime = firstEvent.mTriggerTime + interval;
+ mEventQueue.add(event);
+ } else {
+ long triggerTime = now + event.mPeriod;
+ if (firstEvent.mTriggerTime < triggerTime) {
+ event.mTriggerTime = firstEvent.mTriggerTime;
+ event.mLastTriggerTime -= event.mPeriod;
+ } else {
+ event.mTriggerTime = triggerTime;
+ }
+ mEventQueue.add(event);
+ recalculatePeriods();
+ }
+ }
+
+ /**
+ * Sets a periodic timer.
+ *
+ * @param period the timer period; in milli-second
+ * @param callback is called back when the timer goes off; the same callback
+ * can be specified in multiple timer events
+ */
+ public synchronized void set(int period, Runnable callback) {
+ if (stopped()) return;
+
+ long now = SystemClock.elapsedRealtime();
+ MyEvent event = new MyEvent(period, callback, now);
+ insertEvent(event);
+
+ if (mEventQueue.first() == event) {
+ if (mEventQueue.size() > 1) cancelAlarm();
+ scheduleNext();
+ }
+
+ long triggerTime = event.mTriggerTime;
+ if (DEBUG_TIMER) {
+ Log.d(TAG, " add event " + event + " scheduled at "
+ + showTime(triggerTime) + " at " + showTime(now)
+ + ", #events=" + mEventQueue.size());
+ printQueue();
+ }
+ }
+
+ /**
+ * Cancels all the timer events with the specified callback.
+ *
+ * @param callback the callback
+ */
+ public synchronized void cancel(Runnable callback) {
+ if (stopped() || mEventQueue.isEmpty()) return;
+ if (DEBUG_TIMER) Log.d(TAG, "cancel:" + callback);
+
+ MyEvent firstEvent = mEventQueue.first();
+ for (Iterator<MyEvent> iter = mEventQueue.iterator();
+ iter.hasNext();) {
+ MyEvent event = iter.next();
+ if (event.mCallback == callback) {
+ iter.remove();
+ if (DEBUG_TIMER) Log.d(TAG, " cancel found:" + event);
+ }
+ }
+ if (mEventQueue.isEmpty()) {
+ cancelAlarm();
+ } else if (mEventQueue.first() != firstEvent) {
+ cancelAlarm();
+ firstEvent = mEventQueue.first();
+ firstEvent.mPeriod = firstEvent.mMaxPeriod;
+ firstEvent.mTriggerTime = firstEvent.mLastTriggerTime
+ + firstEvent.mPeriod;
+ recalculatePeriods();
+ scheduleNext();
+ }
+ if (DEBUG_TIMER) {
+ Log.d(TAG, "after cancel:");
+ printQueue();
+ }
+ }
+
+ private void scheduleNext() {
+ if (stopped() || mEventQueue.isEmpty()) return;
+
+ if (mPendingIntent != null) {
+ throw new RuntimeException("pendingIntent is not null!");
+ }
+
+ MyEvent event = mEventQueue.first();
+ Intent intent = new Intent(getAction());
+ intent.putExtra(TRIGGER_TIME, event.mTriggerTime);
+ PendingIntent pendingIntent = mPendingIntent =
+ PendingIntent.getBroadcast(mContext, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ event.mTriggerTime, pendingIntent);
+ }
+
+ @Override
+ public 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) {
+ if (DEBUG_TIMER) Log.d(TAG, "time's up, triggerTime = "
+ + showTime(triggerTime) + ": " + mEventQueue.size());
+ if (stopped() || mEventQueue.isEmpty()) return;
+
+ for (MyEvent event : mEventQueue) {
+ if (event.mTriggerTime != triggerTime) break;
+ if (DEBUG_TIMER) Log.d(TAG, "execute " + event);
+
+ event.mLastTriggerTime = event.mTriggerTime;
+ event.mTriggerTime += event.mPeriod;
+
+ // run the callback in a new thread to prevent deadlock
+ new Thread(event.mCallback, "SipServiceTimerCallbackThread")
+ .start();
+ }
+ if (DEBUG_TIMER) {
+ Log.d(TAG, "after timeout execution");
+ printQueue();
+ }
+ scheduleNext();
+ }
+
+ private String getAction() {
+ return toString();
+ }
+
+ private String showTime(long time) {
+ int ms = (int) (time % 1000);
+ int s = (int) (time / 1000);
+ int m = s / 60;
+ s %= 60;
+ return String.format("%d.%d.%d", m, s, ms);
+ }
+ }
+
+ private static class MyEvent {
+ int mPeriod;
+ int mMaxPeriod;
+ long mTriggerTime;
+ long mLastTriggerTime;
+ Runnable mCallback;
+
+ MyEvent(int period, Runnable callback, long now) {
+ mPeriod = mMaxPeriod = period;
+ mCallback = callback;
+ mLastTriggerTime = now;
+ }
+
+ @Override
+ public String toString() {
+ String s = super.toString();
+ s = s.substring(s.indexOf("@"));
+ return s + ":" + (mPeriod / 1000) + ":" + (mMaxPeriod / 1000) + ":"
+ + toString(mCallback);
+ }
+
+ private String toString(Object o) {
+ String s = o.toString();
+ int index = s.indexOf("$");
+ if (index > 0) s = s.substring(index + 1);
+ return s;
+ }
+ }
+
+ private static class MyEventComparator implements Comparator<MyEvent> {
+ public int compare(MyEvent e1, MyEvent e2) {
+ if (e1 == e2) return 0;
+ int diff = e1.mMaxPeriod - e2.mMaxPeriod;
+ if (diff == 0) diff = -1;
+ return diff;
+ }
+
+ public boolean equals(Object that) {
+ return (this == that);
+ }
+ }
+
+ // Single-threaded executor
+ private static class MyExecutor extends Handler {
+ MyExecutor() {
+ super(createLooper());
+ }
+
+ private static Looper createLooper() {
+ HandlerThread thread = new HandlerThread("SipService");
+ thread.start();
+ return thread.getLooper();
+ }
+
+ void addTask(Runnable task) {
+ Message.obtain(this, 0/* don't care */, task).sendToTarget();
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.obj instanceof Runnable) {
+ ((Runnable) msg.obj).run();
+ } else {
+ Log.w(TAG, "can't handle msg: " + msg);
+ }
+ }
+ }
+}
diff --git a/voip/java/com/android/server/sip/SipSessionGroup.java b/voip/java/com/android/server/sip/SipSessionGroup.java
new file mode 100644
index 0000000..91677a2
--- /dev/null
+++ b/voip/java/com/android/server/sip/SipSessionGroup.java
@@ -0,0 +1,1393 @@
+/*
+ * 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.ProxyAuthenticate;
+import gov.nist.javax.sip.header.WWWAuthenticate;
+import gov.nist.javax.sip.message.SIPMessage;
+
+import android.net.sip.ISipSession;
+import android.net.sip.ISipSessionListener;
+import android.net.sip.SipErrorCode;
+import android.net.sip.SipProfile;
+import android.net.sip.SipSession;
+import android.net.sip.SipSessionAdapter;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.DatagramSocket;
+import java.net.UnknownHostException;
+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.TransactionUnavailableException;
+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 boolean DEBUG = true;
+ private static final boolean DEBUG_PING = DEBUG && false;
+ private static final String ANONYMOUS = "anonymous";
+ private static final String SERVER_ERROR_PREFIX = "Response: ";
+ private static final int EXPIRY_TIME = 3600; // in seconds
+ private static final int CANCEL_CALL_TIMER = 3; // in seconds
+
+ 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);
+ }
+
+ synchronized 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)) {
+ Log.v(TAG, "outboundProxy is " + 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();
+ }
+
+ synchronized void onConnectivityChanged() {
+ for (SipSessionImpl s : mSessionMap.values()) {
+ s.onError(SipErrorCode.DATA_CONNECTION_LOST,
+ "data connection lost");
+ }
+ }
+
+ 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);
+ SipSessionImpl session = mSessionMap.get(key);
+ if ((session != null) && isLoggable(session)) {
+ Log.d(TAG, "session key from event: " + key);
+ Log.d(TAG, "active sessions:");
+ for (String k : mSessionMap.keySet()) {
+ Log.d(TAG, " ..." + k + ": " + mSessionMap.get(k));
+ }
+ }
+ return ((session != null) ? session : mCallReceiverSession);
+ }
+
+ private synchronized void addSipSession(SipSessionImpl newSession) {
+ removeSipSession(newSession);
+ String key = newSession.getCallId();
+ mSessionMap.put(key, newSession);
+ if (isLoggable(newSession)) {
+ Log.d(TAG, "+++ add a session with key: '" + key + "'");
+ 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);
+ }
+ }
+ }
+
+ if ((s != null) && isLoggable(s)) {
+ Log.d(TAG, "remove session " + session + " @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 {
+ boolean isLoggable = isLoggable(session, event);
+ boolean processed = (session != null) && session.process(event);
+ if (isLoggable && processed) {
+ Log.d(TAG, "new state after: "
+ + SipSession.State.toString(session.mState));
+ }
+ } catch (Throwable e) {
+ Log.w(TAG, "event process error: " + event, e);
+ session.onError(e);
+ }
+ }
+
+ private String extractContent(Message message) {
+ // Currently we do not support secure MIME bodies.
+ byte[] bytes = message.getRawContent();
+ if (bytes != null) {
+ try {
+ if (message instanceof SIPMessage) {
+ return ((SIPMessage) message).getMessageContent();
+ } else {
+ return new String(bytes, "UTF-8");
+ }
+ } catch (UnsupportedEncodingException e) {
+ }
+ }
+ return null;
+ }
+
+ private class SipSessionCallReceiverImpl extends SipSessionImpl {
+ public SipSessionCallReceiverImpl(ISipSessionListener listener) {
+ super(listener);
+ }
+
+ public boolean process(EventObject evt) throws SipException {
+ if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~ " + this + ": "
+ + SipSession.State.toString(mState) + ": processing "
+ + log(evt));
+ if (isRequestEvent(Request.INVITE, evt)) {
+ RequestEvent event = (RequestEvent) evt;
+ SipSessionImpl newSession = new SipSessionImpl(mProxy);
+ newSession.mServerTransaction = mSipHelper.sendRinging(event,
+ generateTag());
+ newSession.mDialog = newSession.mServerTransaction.getDialog();
+ newSession.mInviteReceived = event;
+ newSession.mPeerProfile = createPeerProfile(event.getRequest());
+ newSession.mState = SipSession.State.INCOMING_CALL;
+ newSession.mPeerSessionDescription =
+ extractContent(event.getRequest());
+ addSipSession(newSession);
+ mProxy.onRinging(newSession, newSession.mPeerProfile,
+ newSession.mPeerSessionDescription);
+ return true;
+ } else if (isRequestEvent(Request.OPTIONS, evt)) {
+ mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ class SipSessionImpl extends ISipSession.Stub {
+ SipProfile mPeerProfile;
+ SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
+ int mState = SipSession.State.READY_TO_CALL;
+ RequestEvent mInviteReceived;
+ Dialog mDialog;
+ ServerTransaction mServerTransaction;
+ ClientTransaction mClientTransaction;
+ String mPeerSessionDescription;
+ boolean mInCall;
+ boolean mReRegisterFlag = false;
+ SessionTimer mTimer;
+
+ // lightweight timer
+ class SessionTimer {
+ private boolean mRunning = true;
+
+ void start(final int timeout) {
+ new Thread(new Runnable() {
+ public void run() {
+ sleep(timeout);
+ if (mRunning) timeout();
+ }
+ }, "SipSessionTimerThread").start();
+ }
+
+ synchronized void cancel() {
+ mRunning = false;
+ this.notify();
+ }
+
+ private void timeout() {
+ synchronized (SipSessionGroup.this) {
+ onError(SipErrorCode.TIME_OUT, "Session timed out!");
+ }
+ }
+
+ private synchronized void sleep(int timeout) {
+ try {
+ this.wait(timeout * 1000);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "session timer interrupted!");
+ }
+ }
+ }
+
+ public SipSessionImpl(ISipSessionListener listener) {
+ setListener(listener);
+ }
+
+ SipSessionImpl duplicate() {
+ return new SipSessionImpl(mProxy.getListener());
+ }
+
+ private void reset() {
+ mInCall = false;
+ removeSipSession(this);
+ mPeerProfile = null;
+ mState = SipSession.State.READY_TO_CALL;
+ mInviteReceived = null;
+ mDialog = null;
+ mServerTransaction = null;
+ mClientTransaction = null;
+ mPeerSessionDescription = null;
+
+ cancelSessionTimer();
+ }
+
+ 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 int getState() {
+ return mState;
+ }
+
+ public void setListener(ISipSessionListener listener) {
+ mProxy.setListener((listener instanceof SipSessionListenerProxy)
+ ? ((SipSessionListenerProxy) listener).getListener()
+ : listener);
+ }
+
+ // process the command in a new thread
+ private void doCommandAsync(final EventObject command) {
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ processCommand(command);
+ } catch (SipException e) {
+ Log.w(TAG, "command error: " + command, e);
+ onError(e);
+ }
+ }
+ }, "SipSessionAsyncCmdThread").start();
+ }
+
+ public void makeCall(SipProfile peerProfile, String sessionDescription,
+ int timeout) {
+ doCommandAsync(new MakeCallCommand(peerProfile, sessionDescription,
+ timeout));
+ }
+
+ public void answerCall(String sessionDescription, int timeout) {
+ try {
+ processCommand(new MakeCallCommand(mPeerProfile,
+ sessionDescription, timeout));
+ } catch (SipException e) {
+ onError(e);
+ }
+ }
+
+ public void endCall() {
+ doCommandAsync(END_CALL);
+ }
+
+ public void changeCall(String sessionDescription, int timeout) {
+ doCommandAsync(new MakeCallCommand(mPeerProfile, sessionDescription,
+ timeout));
+ }
+
+ public void changeCallWithTimeout(
+ String sessionDescription, int timeout) {
+ doCommandAsync(new MakeCallCommand(mPeerProfile, sessionDescription,
+ timeout));
+ }
+
+ public void register(int duration) {
+ doCommandAsync(new RegisterCommand(duration));
+ }
+
+ public void unregister() {
+ doCommandAsync(DEREGISTER);
+ }
+
+ public boolean isReRegisterRequired() {
+ return mReRegisterFlag;
+ }
+
+ public void clearReRegisterRequired() {
+ mReRegisterFlag = false;
+ }
+
+ public void sendKeepAlive() {
+ mState = SipSession.State.PINGING;
+ try {
+ processCommand(new OptionsCommand());
+ while (SipSession.State.PINGING == 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)) {
+ onError(SipErrorCode.IN_PROGRESS,
+ "cannot initiate a new transaction 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("@")) + ":"
+ + SipSession.State.toString(mState);
+ } catch (Throwable e) {
+ return super.toString();
+ }
+ }
+
+ public boolean process(EventObject evt) throws SipException {
+ if (isLoggable(this, evt)) Log.d(TAG, " ~~~~~ " + this + ": "
+ + SipSession.State.toString(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 SipSession.State.REGISTERING:
+ case SipSession.State.DEREGISTERING:
+ processed = registeringToReady(evt);
+ break;
+ case SipSession.State.PINGING:
+ processed = keepAliveProcess(evt);
+ break;
+ case SipSession.State.READY_TO_CALL:
+ processed = readyForCall(evt);
+ break;
+ case SipSession.State.INCOMING_CALL:
+ processed = incomingCall(evt);
+ break;
+ case SipSession.State.INCOMING_CALL_ANSWERING:
+ processed = incomingCallToInCall(evt);
+ break;
+ case SipSession.State.OUTGOING_CALL:
+ case SipSession.State.OUTGOING_CALL_RING_BACK:
+ processed = outgoingCall(evt);
+ break;
+ case SipSession.State.OUTGOING_CALL_CANCELING:
+ processed = outgoingCallToReady(evt);
+ break;
+ case SipSession.State.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 (isCurrentTransaction((TransactionTerminatedEvent) evt)) {
+ if (evt instanceof TimeoutEvent) {
+ processTimeout((TimeoutEvent) evt);
+ } else {
+ processTransactionTerminated(
+ (TransactionTerminatedEvent) evt);
+ }
+ return true;
+ }
+ } else if (isRequestEvent(Request.OPTIONS, evt)) {
+ mSipHelper.sendResponse((RequestEvent) evt, Response.OK);
+ 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 boolean isCurrentTransaction(TransactionTerminatedEvent event) {
+ Transaction current = event.isServerTransaction()
+ ? mServerTransaction
+ : mClientTransaction;
+ Transaction target = event.isServerTransaction()
+ ? event.getServerTransaction()
+ : event.getClientTransaction();
+
+ if ((current != target) && (mState != SipSession.State.PINGING)) {
+ Log.d(TAG, "not the current transaction; current="
+ + toString(current) + ", target=" + toString(target));
+ return false;
+ } else if (current != null) {
+ Log.d(TAG, "transaction terminated: " + toString(current));
+ return true;
+ } else {
+ // no transaction; shouldn't be here; ignored
+ return true;
+ }
+ }
+
+ private String toString(Transaction transaction) {
+ if (transaction == null) return "null";
+ Request request = transaction.getRequest();
+ Dialog dialog = transaction.getDialog();
+ CSeqHeader cseq = (CSeqHeader) request.getHeader(CSeqHeader.NAME);
+ return String.format("req=%s,%s,s=%s,ds=%s,", request.getMethod(),
+ cseq.getSeqNumber(), transaction.getState(),
+ ((dialog == null) ? "-" : dialog.getState()));
+ }
+
+ private void processTransactionTerminated(
+ TransactionTerminatedEvent event) {
+ switch (mState) {
+ case SipSession.State.IN_CALL:
+ case SipSession.State.READY_TO_CALL:
+ Log.d(TAG, "Transaction terminated; do nothing");
+ break;
+ default:
+ Log.d(TAG, "Transaction terminated early: " + this);
+ onError(SipErrorCode.TRANSACTION_TERMINTED,
+ "transaction terminated");
+ }
+ }
+
+ private void processTimeout(TimeoutEvent event) {
+ Log.d(TAG, "processing Timeout...");
+ switch (mState) {
+ case SipSession.State.REGISTERING:
+ case SipSession.State.DEREGISTERING:
+ reset();
+ mProxy.onRegistrationTimeout(this);
+ break;
+ case SipSession.State.INCOMING_CALL:
+ case SipSession.State.INCOMING_CALL_ANSWERING:
+ case SipSession.State.OUTGOING_CALL:
+ case SipSession.State.OUTGOING_CALL_CANCELING:
+ onError(SipErrorCode.TIME_OUT, event.toString());
+ break;
+ case SipSession.State.PINGING:
+ reset();
+ mReRegisterFlag = true;
+ mState = SipSession.State.READY_TO_CALL;
+ break;
+
+ default:
+ Log.d(TAG, " 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;
+ if (DEBUG) Log.w(TAG, String.format(
+ "rport is changed: %d <> %d", mRPort, rPort));
+ mRPort = rPort;
+ } else {
+ if (DEBUG_PING) Log.w(TAG, "rport is the same: " + rPort);
+ }
+ } else {
+ if (DEBUG) Log.w(TAG, "peer did not respond rport");
+ }
+ reset();
+ return true;
+ }
+ return false;
+ }
+
+ private int getRPortFromResponse(Response response) {
+ ViaHeader viaHeader = (ViaHeader)(response.getHeader(
+ SIPHeaderNames.VIA));
+ return (viaHeader == null) ? -1 : viaHeader.getRPort();
+ }
+
+ private boolean registeringToReady(EventObject evt)
+ throws SipException {
+ if (expectResponse(Request.REGISTER, evt)) {
+ ResponseEvent event = (ResponseEvent) evt;
+ Response response = event.getResponse();
+
+ int statusCode = response.getStatusCode();
+ switch (statusCode) {
+ case Response.OK:
+ int state = mState;
+ onRegistrationDone((state == SipSession.State.REGISTERING)
+ ? getExpiryTime(((ResponseEvent) evt).getResponse())
+ : -1);
+ mLastNonce = null;
+ mRPort = 0;
+ return true;
+ case Response.UNAUTHORIZED:
+ case Response.PROXY_AUTHENTICATION_REQUIRED:
+ if (!handleAuthentication(event)) {
+ if (mLastNonce == null) {
+ onRegistrationFailed(SipErrorCode.SERVER_ERROR,
+ "server does not provide challenge");
+ } else {
+ Log.v(TAG, "Incorrect username/password");
+ onRegistrationFailed(
+ SipErrorCode.INVALID_CREDENTIALS,
+ "incorrect username or password");
+ }
+ }
+ return true;
+ default:
+ if (statusCode >= 500) {
+ onRegistrationFailed(response);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean handleAuthentication(ResponseEvent event)
+ throws SipException {
+ Response response = event.getResponse();
+ String nonce = getNonceFromResponse(response);
+ if (((nonce != null) && nonce.equals(mLastNonce)) ||
+ (nonce == null)) {
+ mLastNonce = nonce;
+ return false;
+ } else {
+ mClientTransaction = mSipHelper.handleChallenge(
+ event, getAccountManager());
+ mDialog = mClientTransaction.getDialog();
+ mLastNonce = nonce;
+ return true;
+ }
+ }
+
+ private boolean crossDomainAuthenticationRequired(Response response) {
+ String realm = getRealmFromResponse(response);
+ if (realm == null) realm = "";
+ return !mLocalProfile.getSipDomain().trim().equals(realm.trim());
+ }
+
+ 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 getRealmFromResponse(Response response) {
+ WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
+ SIPHeaderNames.WWW_AUTHENTICATE);
+ if (wwwAuth != null) return wwwAuth.getRealm();
+ ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
+ SIPHeaderNames.PROXY_AUTHENTICATE);
+ return (proxyAuth == null) ? null : proxyAuth.getRealm();
+ }
+
+ private String getNonceFromResponse(Response response) {
+ WWWAuthenticate wwwAuth = (WWWAuthenticate)response.getHeader(
+ SIPHeaderNames.WWW_AUTHENTICATE);
+ if (wwwAuth != null) return wwwAuth.getNonce();
+ ProxyAuthenticate proxyAuth = (ProxyAuthenticate)response.getHeader(
+ SIPHeaderNames.PROXY_AUTHENTICATE);
+ return (proxyAuth == null) ? null : proxyAuth.getNonce();
+ }
+
+ private boolean readyForCall(EventObject evt) throws SipException {
+ // expect MakeCallCommand, RegisterCommand, DEREGISTER
+ if (evt instanceof MakeCallCommand) {
+ MakeCallCommand cmd = (MakeCallCommand) evt;
+ mPeerProfile = cmd.getPeerProfile();
+ mClientTransaction = mSipHelper.sendInvite(mLocalProfile,
+ mPeerProfile, cmd.getSessionDescription(),
+ generateTag());
+ mDialog = mClientTransaction.getDialog();
+ addSipSession(this);
+ mState = SipSession.State.OUTGOING_CALL;
+ mProxy.onCalling(this);
+ startSessionTimer(cmd.getTimeout());
+ return true;
+ } else if (evt instanceof RegisterCommand) {
+ int duration = ((RegisterCommand) evt).getDuration();
+ mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
+ generateTag(), duration);
+ mDialog = mClientTransaction.getDialog();
+ addSipSession(this);
+ mState = SipSession.State.REGISTERING;
+ mProxy.onRegistering(this);
+ return true;
+ } else if (DEREGISTER == evt) {
+ mClientTransaction = mSipHelper.sendRegister(mLocalProfile,
+ generateTag(), 0);
+ mDialog = mClientTransaction.getDialog();
+ addSipSession(this);
+ mState = SipSession.State.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 = SipSession.State.INCOMING_CALL_ANSWERING;
+ startSessionTimer(((MakeCallCommand) evt).getTimeout());
+ 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 == SipSession.State.OUTGOING_CALL) {
+ mState = SipSession.State.OUTGOING_CALL_RING_BACK;
+ mProxy.onRingingBack(this);
+ cancelSessionTimer();
+ }
+ return true;
+ case Response.OK:
+ mSipHelper.sendInviteAck(event, mDialog);
+ mPeerSessionDescription = extractContent(response);
+ establishCall();
+ return true;
+ case Response.UNAUTHORIZED:
+ case Response.PROXY_AUTHENTICATION_REQUIRED:
+ if (crossDomainAuthenticationRequired(response)) {
+ onError(SipErrorCode.CROSS_DOMAIN_AUTHENTICATION,
+ getRealmFromResponse(response));
+ } else if (handleAuthentication(event)) {
+ addSipSession(this);
+ } else if (mLastNonce == null) {
+ onError(SipErrorCode.SERVER_ERROR,
+ "server does not provide challenge");
+ } else {
+ onError(SipErrorCode.INVALID_CREDENTIALS,
+ "incorrect username or password");
+ }
+ 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(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 = SipSession.State.OUTGOING_CALL_CANCELING;
+ startSessionTimer(CANCEL_CALL_TIMER);
+ 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)) {
+ switch (statusCode) {
+ case Response.OK:
+ outgoingCall(evt); // abort Cancel
+ return true;
+ case Response.REQUEST_TERMINATED:
+ endCallNormally();
+ return true;
+ }
+ } else {
+ return false;
+ }
+
+ if (statusCode >= 400) {
+ onError(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 = SipSession.State.INCOMING_CALL;
+ mPeerSessionDescription = extractContent(event.getRequest());
+ 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 = SipSession.State.OUTGOING_CALL;
+ startSessionTimer(((MakeCallCommand) evt).getTimeout());
+ return true;
+ }
+ return false;
+ }
+
+ // timeout in seconds
+ private void startSessionTimer(int timeout) {
+ if (timeout > 0) {
+ mTimer = new SessionTimer();
+ mTimer.start(timeout);
+ }
+ }
+
+ private void cancelSessionTimer() {
+ if (mTimer != null) {
+ mTimer.cancel();
+ mTimer = null;
+ }
+ }
+
+ private String createErrorMessage(Response response) {
+ return String.format(SERVER_ERROR_PREFIX + "%s (%d)",
+ response.getReasonPhrase(), response.getStatusCode());
+ }
+
+ private void establishCall() {
+ mState = SipSession.State.IN_CALL;
+ mInCall = true;
+ cancelSessionTimer();
+ mProxy.onCallEstablished(this, mPeerSessionDescription);
+ }
+
+ private void fallbackToPreviousInCall(int errorCode, String message) {
+ mState = SipSession.State.IN_CALL;
+ mProxy.onCallChangeFailed(this, errorCode, message);
+ }
+
+ private void endCallNormally() {
+ reset();
+ mProxy.onCallEnded(this);
+ }
+
+ private void endCallOnError(int errorCode, String message) {
+ reset();
+ mProxy.onError(this, errorCode, message);
+ }
+
+ private void endCallOnBusy() {
+ reset();
+ mProxy.onCallBusy(this);
+ }
+
+ private void onError(int errorCode, String message) {
+ cancelSessionTimer();
+ switch (mState) {
+ case SipSession.State.REGISTERING:
+ case SipSession.State.DEREGISTERING:
+ onRegistrationFailed(errorCode, message);
+ break;
+ default:
+ if ((errorCode != SipErrorCode.DATA_CONNECTION_LOST)
+ && mInCall) {
+ fallbackToPreviousInCall(errorCode, message);
+ } else {
+ endCallOnError(errorCode, message);
+ }
+ }
+ }
+
+
+ private void onError(Throwable exception) {
+ exception = getRootCause(exception);
+ onError(getErrorCode(exception), exception.toString());
+ }
+
+ private void onError(Response response) {
+ int statusCode = response.getStatusCode();
+ if (!mInCall && (statusCode == Response.BUSY_HERE)) {
+ endCallOnBusy();
+ } else {
+ onError(getErrorCode(statusCode), createErrorMessage(response));
+ }
+ }
+
+ private int getErrorCode(int responseStatusCode) {
+ switch (responseStatusCode) {
+ case Response.TEMPORARILY_UNAVAILABLE:
+ case Response.FORBIDDEN:
+ case Response.GONE:
+ case Response.NOT_FOUND:
+ case Response.NOT_ACCEPTABLE:
+ case Response.NOT_ACCEPTABLE_HERE:
+ return SipErrorCode.PEER_NOT_REACHABLE;
+
+ case Response.REQUEST_URI_TOO_LONG:
+ case Response.ADDRESS_INCOMPLETE:
+ case Response.AMBIGUOUS:
+ return SipErrorCode.INVALID_REMOTE_URI;
+
+ case Response.REQUEST_TIMEOUT:
+ return SipErrorCode.TIME_OUT;
+
+ default:
+ if (responseStatusCode < 500) {
+ return SipErrorCode.CLIENT_ERROR;
+ } else {
+ return SipErrorCode.SERVER_ERROR;
+ }
+ }
+ }
+
+ private Throwable getRootCause(Throwable exception) {
+ Throwable cause = exception.getCause();
+ while (cause != null) {
+ exception = cause;
+ cause = exception.getCause();
+ }
+ return exception;
+ }
+
+ private int getErrorCode(Throwable exception) {
+ String message = exception.getMessage();
+ if (exception instanceof UnknownHostException) {
+ return SipErrorCode.INVALID_REMOTE_URI;
+ } else if (exception instanceof IOException) {
+ return SipErrorCode.SOCKET_ERROR;
+ } else if (message.startsWith(SERVER_ERROR_PREFIX)) {
+ return SipErrorCode.SERVER_ERROR;
+ } else {
+ return SipErrorCode.CLIENT_ERROR;
+ }
+ }
+
+ private void onRegistrationDone(int duration) {
+ reset();
+ mProxy.onRegistrationDone(this, duration);
+ }
+
+ private void onRegistrationFailed(int errorCode, String message) {
+ reset();
+ mProxy.onRegistrationFailed(this, errorCode, message);
+ }
+
+ private void onRegistrationFailed(Throwable exception) {
+ reset();
+ exception = getRootCause(exception);
+ onRegistrationFailed(getErrorCode(exception),
+ exception.toString());
+ }
+
+ private void onRegistrationFailed(Response response) {
+ reset();
+ int statusCode = response.getStatusCode();
+ onRegistrationFailed(getErrorCode(statusCode),
+ createErrorMessage(response));
+ }
+ }
+
+ /**
+ * @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 (IllegalArgumentException e) {
+ throw new SipException("createPeerProfile()", e);
+ } catch (ParseException e) {
+ throw new SipException("createPeerProfile()", e);
+ }
+ }
+
+ private static boolean isLoggable(SipSessionImpl s) {
+ if (s != null) {
+ switch (s.mState) {
+ case SipSession.State.PINGING:
+ return DEBUG_PING;
+ }
+ }
+ return DEBUG;
+ }
+
+ private static boolean isLoggable(SipSessionImpl s, EventObject evt) {
+ if (!isLoggable(s)) return false;
+ if (evt == null) return false;
+
+ if (evt instanceof OptionsCommand) {
+ return DEBUG_PING;
+ } else if (evt instanceof ResponseEvent) {
+ Response response = ((ResponseEvent) evt).getResponse();
+ if (Request.OPTIONS.equals(response.getHeader(CSeqHeader.NAME))) {
+ return DEBUG_PING;
+ }
+ return DEBUG;
+ } else if (evt instanceof RequestEvent) {
+ return DEBUG;
+ }
+ return false;
+ }
+
+ 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 String mSessionDescription;
+ private int mTimeout; // in seconds
+
+ public MakeCallCommand(SipProfile peerProfile,
+ String sessionDescription) {
+ this(peerProfile, sessionDescription, -1);
+ }
+
+ public MakeCallCommand(SipProfile peerProfile,
+ String sessionDescription, int timeout) {
+ super(peerProfile);
+ mSessionDescription = sessionDescription;
+ mTimeout = timeout;
+ }
+
+ public SipProfile getPeerProfile() {
+ return (SipProfile) getSource();
+ }
+
+ public String getSessionDescription() {
+ return mSessionDescription;
+ }
+
+ public int getTimeout() {
+ return mTimeout;
+ }
+ }
+}
diff --git a/voip/java/com/android/server/sip/SipSessionListenerProxy.java b/voip/java/com/android/server/sip/SipSessionListenerProxy.java
new file mode 100644
index 0000000..f8be0a8
--- /dev/null
+++ b/voip/java/com/android/server/sip/SipSessionListenerProxy.java
@@ -0,0 +1,217 @@
+/*
+ * 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.os.DeadObjectException;
+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, "SipSessionCallbackThread").start();
+ }
+
+ public void onCalling(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onCalling(session);
+ } catch (Throwable t) {
+ handle(t, "onCalling()");
+ }
+ }
+ });
+ }
+
+ public void onRinging(final ISipSession session, final SipProfile caller,
+ final String sessionDescription) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onRinging(session, caller, sessionDescription);
+ } catch (Throwable t) {
+ handle(t, "onRinging()");
+ }
+ }
+ });
+ }
+
+ public void onRingingBack(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onRingingBack(session);
+ } catch (Throwable t) {
+ handle(t, "onRingingBack()");
+ }
+ }
+ });
+ }
+
+ public void onCallEstablished(final ISipSession session,
+ final String sessionDescription) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onCallEstablished(session, sessionDescription);
+ } catch (Throwable t) {
+ handle(t, "onCallEstablished()");
+ }
+ }
+ });
+ }
+
+ public void onCallEnded(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onCallEnded(session);
+ } catch (Throwable t) {
+ handle(t, "onCallEnded()");
+ }
+ }
+ });
+ }
+
+ public void onCallBusy(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onCallBusy(session);
+ } catch (Throwable t) {
+ handle(t, "onCallBusy()");
+ }
+ }
+ });
+ }
+
+ public void onCallChangeFailed(final ISipSession session,
+ final int errorCode, final String message) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onCallChangeFailed(session, errorCode, message);
+ } catch (Throwable t) {
+ handle(t, "onCallChangeFailed()");
+ }
+ }
+ });
+ }
+
+ public void onError(final ISipSession session, final int errorCode,
+ final String message) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onError(session, errorCode, message);
+ } catch (Throwable t) {
+ handle(t, "onError()");
+ }
+ }
+ });
+ }
+
+ public void onRegistering(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onRegistering(session);
+ } catch (Throwable t) {
+ handle(t, "onRegistering()");
+ }
+ }
+ });
+ }
+
+ 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) {
+ handle(t, "onRegistrationDone()");
+ }
+ }
+ });
+ }
+
+ public void onRegistrationFailed(final ISipSession session,
+ final int errorCode, final String message) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onRegistrationFailed(session, errorCode, message);
+ } catch (Throwable t) {
+ handle(t, "onRegistrationFailed()");
+ }
+ }
+ });
+ }
+
+ public void onRegistrationTimeout(final ISipSession session) {
+ if (mListener == null) return;
+ proxy(new Runnable() {
+ public void run() {
+ try {
+ mListener.onRegistrationTimeout(session);
+ } catch (Throwable t) {
+ handle(t, "onRegistrationTimeout()");
+ }
+ }
+ });
+ }
+
+ private void handle(Throwable t, String message) {
+ if (t instanceof DeadObjectException) {
+ mListener = null;
+ // This creates race but it's harmless. Just don't log the error
+ // when it happens.
+ } else if (mListener != null) {
+ Log.w(TAG, message, t);
+ }
+ }
+}