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