diff options
author | Hung-ying Tyan <tyanh@google.com> | 2010-09-25 22:49:59 +0800 |
---|---|---|
committer | Hung-ying Tyan <tyanh@google.com> | 2010-09-28 05:19:35 +0800 |
commit | 7e54ef71db3320a751571bba5259fba816399421 (patch) | |
tree | ac4c1a7d45c1406366e7f7ba02ec0b0d6117d386 /voip | |
parent | 04a426f5b50a628fb73bc29b003b0e5bece17c27 (diff) | |
download | frameworks_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.java | 464 | ||||
-rw-r--r-- | voip/java/com/android/server/sip/SipService.java | 1234 | ||||
-rw-r--r-- | voip/java/com/android/server/sip/SipSessionGroup.java | 1393 | ||||
-rw-r--r-- | voip/java/com/android/server/sip/SipSessionListenerProxy.java | 217 |
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); + } + } +} |