/* * 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.AppOpsManager; 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.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.telephony.Rlog; import java.io.IOException; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; import javax.sip.SipException; /** * @hide */ public final class SipService extends ISipService.Stub { static final String TAG = "SipService"; static final boolean DBG = true; private static final int EXPIRY_TIME = 3600; private static final int SHORT_EXPIRY_TIME = 10; private static final int MIN_EXPIRY_TIME = 60; private static final int DEFAULT_KEEPALIVE_INTERVAL = 10; // in seconds private static final int DEFAULT_MAX_KEEPALIVE_INTERVAL = 120; // in seconds private Context mContext; private String mLocalIp; private int mNetworkType = -1; private SipWakeupTimer mTimer; private WifiManager.WifiLock mWifiLock; private boolean mSipOnWifiOnly; private final AppOpsManager mAppOps; private SipKeepAliveProcessCallback mSipKeepAliveProcessCallback; private MyExecutor mExecutor = new MyExecutor(); // SipProfile URI --> group private Map mSipGroups = new HashMap(); // session ID --> session private Map mPendingSessions = new HashMap(); private ConnectivityReceiver mConnectivityReceiver; private SipWakeLock mMyWakeLock; private int mKeepAliveInterval; private int mLastGoodKeepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL; /** * 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)) { if (ServiceManager.getService("sip") == null) { ServiceManager.addService("sip", new SipService(context)); context.sendBroadcast(new Intent(SipManager.ACTION_SIP_SERVICE_UP)); if (DBG) slog("start:"); } } } private SipService(Context context) { if (DBG) log("SipService: started!"); mContext = context; mConnectivityReceiver = new ConnectivityReceiver(); mWifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)) .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); mWifiLock.setReferenceCounted(false); mSipOnWifiOnly = SipManager.isSipWifiOnly(context); mMyWakeLock = new SipWakeLock((PowerManager) context.getSystemService(Context.POWER_SERVICE)); mTimer = new SipWakeupTimer(context, mExecutor); mAppOps = mContext.getSystemService(AppOpsManager.class); } @Override public synchronized SipProfile[] getListOfProfiles(String opPackageName) { if (!canUseSip(opPackageName, "getListOfProfiles")) { return new SipProfile[0]; } boolean isCallerRadio = isCallerRadio(); ArrayList profiles = new ArrayList(); for (SipSessionGroupExt group : mSipGroups.values()) { if (isCallerRadio || isCallerCreator(group)) { profiles.add(group.getLocalProfile()); } } return profiles.toArray(new SipProfile[profiles.size()]); } @Override public synchronized void open(SipProfile localProfile, String opPackageName) { if (!canUseSip(opPackageName, "open")) { return; } localProfile.setCallingUid(Binder.getCallingUid()); try { createGroup(localProfile); } catch (SipException e) { loge("openToMakeCalls()", e); // TODO: how to send the exception back } } @Override public synchronized void open3(SipProfile localProfile, PendingIntent incomingCallPendingIntent, ISipSessionListener listener, String opPackageName) { if (!canUseSip(opPackageName, "open3")) { return; } localProfile.setCallingUid(Binder.getCallingUid()); if (incomingCallPendingIntent == null) { if (DBG) log("open3: incomingCallPendingIntent cannot be null; " + "the profile is not opened"); return; } if (DBG) log("open3: " + obfuscateSipUri(localProfile.getUriString()) + ": " + incomingCallPendingIntent + ": " + listener); try { SipSessionGroupExt group = createGroup(localProfile, incomingCallPendingIntent, listener); if (localProfile.getAutoRegistration()) { group.openToReceiveCalls(); updateWakeLocks(); } } catch (SipException e) { loge("open3:", e); // TODO: how to send the exception back } } private boolean isCallerCreator(SipSessionGroupExt group) { SipProfile profile = group.getLocalProfile(); return (profile.getCallingUid() == Binder.getCallingUid()); } private boolean isCallerCreatorOrRadio(SipSessionGroupExt group) { return (isCallerRadio() || isCallerCreator(group)); } private boolean isCallerRadio() { return (Binder.getCallingUid() == Process.PHONE_UID); } @Override public synchronized void close(String localProfileUri, String opPackageName) { if (!canUseSip(opPackageName, "close")) { return; } SipSessionGroupExt group = mSipGroups.get(localProfileUri); if (group == null) return; if (!isCallerCreatorOrRadio(group)) { if (DBG) log("only creator or radio can close this profile"); return; } group = mSipGroups.remove(localProfileUri); notifyProfileRemoved(group.getLocalProfile()); group.close(); updateWakeLocks(); } @Override public synchronized boolean isOpened(String localProfileUri, String opPackageName) { if (!canUseSip(opPackageName, "isOpened")) { return false; } SipSessionGroupExt group = mSipGroups.get(localProfileUri); if (group == null) return false; if (isCallerCreatorOrRadio(group)) { return true; } else { if (DBG) log("only creator or radio can query on the profile"); return false; } } @Override public synchronized boolean isRegistered(String localProfileUri, String opPackageName) { if (!canUseSip(opPackageName, "isRegistered")) { return false; } SipSessionGroupExt group = mSipGroups.get(localProfileUri); if (group == null) return false; if (isCallerCreatorOrRadio(group)) { return group.isRegistered(); } else { if (DBG) log("only creator or radio can query on the profile"); return false; } } @Override public synchronized void setRegistrationListener(String localProfileUri, ISipSessionListener listener, String opPackageName) { if (!canUseSip(opPackageName, "setRegistrationListener")) { return; } SipSessionGroupExt group = mSipGroups.get(localProfileUri); if (group == null) return; if (isCallerCreator(group)) { group.setListener(listener); } else { if (DBG) log("only creator can set listener on the profile"); } } @Override public synchronized ISipSession createSession(SipProfile localProfile, ISipSessionListener listener, String opPackageName) { if (DBG) log("createSession: profile" + localProfile); if (!canUseSip(opPackageName, "createSession")) { return null; } localProfile.setCallingUid(Binder.getCallingUid()); if (mNetworkType == -1) { if (DBG) log("createSession: mNetworkType==-1 ret=null"); return null; } try { SipSessionGroupExt group = createGroup(localProfile); return group.createSession(listener); } catch (SipException e) { if (DBG) loge("createSession;", e); return null; } } @Override public synchronized ISipSession getPendingSession(String callId, String opPackageName) { if (!canUseSip(opPackageName, "getPendingSession")) { return null; } 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) { if (DBG) loge("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); } else if (!isCallerCreator(group)) { throw new SipException("only creator can access the profile"); } return group; } private SipSessionGroupExt createGroup(SipProfile localProfile, PendingIntent incomingCallPendingIntent, ISipSessionListener listener) throws SipException { String key = localProfile.getUriString(); SipSessionGroupExt group = mSipGroups.get(key); if (group != null) { if (!isCallerCreator(group)) { throw new SipException("only creator can access the profile"); } group.setIncomingCallPendingIntent(incomingCallPendingIntent); group.setListener(listener); } else { group = new SipSessionGroupExt(localProfile, incomingCallPendingIntent, listener); mSipGroups.put(key, group); notifyProfileAdded(localProfile); } return group; } private void notifyProfileAdded(SipProfile localProfile) { if (DBG) log("notify: profile added: " + localProfile); Intent intent = new Intent(SipManager.ACTION_SIP_ADD_PHONE); intent.putExtra(SipManager.EXTRA_LOCAL_URI, localProfile.getUriString()); mContext.sendBroadcast(intent); if (mSipGroups.size() == 1) { registerReceivers(); } } private void notifyProfileRemoved(SipProfile localProfile) { if (DBG) log("notify: profile removed: " + localProfile); Intent intent = new Intent(SipManager.ACTION_SIP_REMOVE_PHONE); intent.putExtra(SipManager.EXTRA_LOCAL_URI, localProfile.getUriString()); mContext.sendBroadcast(intent); if (mSipGroups.size() == 0) { unregisterReceivers(); } } private void stopPortMappingMeasurement() { if (mSipKeepAliveProcessCallback != null) { mSipKeepAliveProcessCallback.stop(); mSipKeepAliveProcessCallback = null; } } private void startPortMappingLifetimeMeasurement( SipProfile localProfile) { startPortMappingLifetimeMeasurement(localProfile, DEFAULT_MAX_KEEPALIVE_INTERVAL); } private void startPortMappingLifetimeMeasurement( SipProfile localProfile, int maxInterval) { if ((mSipKeepAliveProcessCallback == null) && (mKeepAliveInterval == -1) && isBehindNAT(mLocalIp)) { if (DBG) log("startPortMappingLifetimeMeasurement: profile=" + localProfile.getUriString()); int minInterval = mLastGoodKeepAliveInterval; if (minInterval >= maxInterval) { // If mLastGoodKeepAliveInterval also does not work, reset it // to the default min minInterval = mLastGoodKeepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL; log(" reset min interval to " + minInterval); } mSipKeepAliveProcessCallback = new SipKeepAliveProcessCallback( localProfile, minInterval, maxInterval); mSipKeepAliveProcessCallback.start(); } } private void restartPortMappingLifetimeMeasurement( SipProfile localProfile, int maxInterval) { stopPortMappingMeasurement(); mKeepAliveInterval = -1; startPortMappingLifetimeMeasurement(localProfile, maxInterval); } private synchronized void addPendingSession(ISipSession session) { try { cleanUpPendingSessions(); mPendingSessions.put(session.getCallId(), session); if (DBG) log("#pending sess=" + mPendingSessions.size()); } catch (RemoteException e) { // should not happen with a local call loge("addPendingSession()", e); } } private void cleanUpPendingSessions() throws RemoteException { Map.Entry[] entries = mPendingSessions.entrySet().toArray( new Map.Entry[mPendingSessions.size()]); for (Map.Entry entry : entries) { if (entry.getValue().getState() != SipSession.State.INCOMING_CALL) { mPendingSessions.remove(entry.getKey()); } } } private synchronized boolean callingSelf(SipSessionGroupExt ringingGroup, SipSessionGroup.SipSessionImpl ringingSession) { String callId = ringingSession.getCallId(); for (SipSessionGroupExt group : mSipGroups.values()) { if ((group != ringingGroup) && group.containsSession(callId)) { if (DBG) log("call self: " + ringingSession.getLocalProfile().getUriString() + " -> " + group.getLocalProfile().getUriString()); return true; } } return false; } private synchronized void onKeepAliveIntervalChanged() { for (SipSessionGroupExt group : mSipGroups.values()) { group.onKeepAliveIntervalChanged(); } } private int getKeepAliveInterval() { return (mKeepAliveInterval < 0) ? mLastGoodKeepAliveInterval : mKeepAliveInterval; } private boolean isBehindNAT(String address) { try { // TODO: How is isBehindNAT used and why these constanst address: // 10.x.x.x | 192.168.x.x | 172.16.x.x .. 172.19.x.x byte[] d = InetAddress.getByName(address).getAddress(); if ((d[0] == 10) || (((0x000000FF & d[0]) == 172) && ((0x000000F0 & d[1]) == 16)) || (((0x000000FF & d[0]) == 192) && ((0x000000FF & d[1]) == 168))) { return true; } } catch (UnknownHostException e) { loge("isBehindAT()" + address, e); } return false; } private boolean canUseSip(String packageName, String message) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.USE_SIP, message); return mAppOps.noteOp(AppOpsManager.OP_USE_SIP, Binder.getCallingUid(), packageName) == AppOpsManager.MODE_ALLOWED; } private class SipSessionGroupExt extends SipSessionAdapter { private static final String SSGE_TAG = "SipSessionGroupExt"; private static final boolean SSGE_DBG = true; private SipSessionGroup mSipGroup; private PendingIntent mIncomingCallPendingIntent; private boolean mOpenedToReceiveCalls; private SipAutoReg mAutoRegistration = new SipAutoReg(); public SipSessionGroupExt(SipProfile localProfile, PendingIntent incomingCallPendingIntent, ISipSessionListener listener) throws SipException { if (SSGE_DBG) log("SipSessionGroupExt: profile=" + localProfile); mSipGroup = new SipSessionGroup(duplicate(localProfile), localProfile.getPassword(), mTimer, mMyWakeLock); mIncomingCallPendingIntent = incomingCallPendingIntent; mAutoRegistration.setListener(listener); } public SipProfile getLocalProfile() { return mSipGroup.getLocalProfile(); } public boolean containsSession(String callId) { return mSipGroup.containsSession(callId); } public void onKeepAliveIntervalChanged() { mAutoRegistration.onKeepAliveIntervalChanged(); } // TODO: remove this method once SipWakeupTimer can better handle variety // of timeout values void setWakeupTimer(SipWakeupTimer timer) { mSipGroup.setWakeupTimer(timer); } private SipProfile duplicate(SipProfile p) { try { return new SipProfile.Builder(p).setPassword("*").build(); } catch (Exception e) { loge("duplicate()", e); throw new RuntimeException("duplicate profile", e); } } public void setListener(ISipSessionListener listener) { mAutoRegistration.setListener(listener); } public void setIncomingCallPendingIntent(PendingIntent pIntent) { mIncomingCallPendingIntent = pIntent; } public void openToReceiveCalls() { mOpenedToReceiveCalls = true; if (mNetworkType != -1) { mSipGroup.openToReceiveCalls(this); mAutoRegistration.start(mSipGroup); } if (SSGE_DBG) log("openToReceiveCalls: " + obfuscateSipUri(getUri()) + ": " + mIncomingCallPendingIntent); } public void onConnectivityChanged(boolean connected) throws SipException { if (SSGE_DBG) { log("onConnectivityChanged: connected=" + connected + " uri=" + obfuscateSipUri(getUri()) + ": " + mIncomingCallPendingIntent); } mSipGroup.onConnectivityChanged(); if (connected) { mSipGroup.reset(); if (mOpenedToReceiveCalls) openToReceiveCalls(); } else { mSipGroup.close(); mAutoRegistration.stop(); } } public void close() { mOpenedToReceiveCalls = false; mSipGroup.close(); mAutoRegistration.stop(); if (SSGE_DBG) log("close: " + obfuscateSipUri(getUri()) + ": " + mIncomingCallPendingIntent); } public ISipSession createSession(ISipSessionListener listener) { if (SSGE_DBG) log("createSession"); return mSipGroup.createSession(listener); } @Override public void onRinging(ISipSession s, SipProfile caller, String sessionDescription) { SipSessionGroup.SipSessionImpl session = (SipSessionGroup.SipSessionImpl) s; synchronized (SipService.this) { try { if (!isRegistered() || callingSelf(this, session)) { if (SSGE_DBG) log("onRinging: end notReg or self"); session.endCall(); return; } // send out incoming call broadcast addPendingSession(session); Intent intent = SipManager.createIncomingCallBroadcast( session.getCallId(), sessionDescription); if (SSGE_DBG) log("onRinging: uri=" + getUri() + ": " + caller.getUri() + ": " + session.getCallId() + " " + mIncomingCallPendingIntent); mIncomingCallPendingIntent.send(mContext, SipManager.INCOMING_CALL_RESULT_CODE, intent); } catch (PendingIntent.CanceledException e) { loge("onRinging: pendingIntent is canceled, drop incoming call", e); session.endCall(); } } } @Override public void onError(ISipSession session, int errorCode, String message) { if (SSGE_DBG) log("onError: errorCode=" + errorCode + " desc=" + SipErrorCode.toString(errorCode) + ": " + message); } public boolean isOpenedToReceiveCalls() { return mOpenedToReceiveCalls; } public boolean isRegistered() { return mAutoRegistration.isRegistered(); } private String getUri() { return mSipGroup.getLocalProfileUri(); } private void log(String s) { Rlog.d(SSGE_TAG, s); } private void loge(String s, Throwable t) { Rlog.e(SSGE_TAG, s, t); } } private class SipKeepAliveProcessCallback implements Runnable, SipSessionGroup.KeepAliveProcessCallback { private static final String SKAI_TAG = "SipKeepAliveProcessCallback"; private static final boolean SKAI_DBG = true; private static final int MIN_INTERVAL = 5; // in seconds private static final int PASS_THRESHOLD = 10; private static final int NAT_MEASUREMENT_RETRY_INTERVAL = 120; // in seconds private SipProfile mLocalProfile; private SipSessionGroupExt mGroup; private SipSessionGroup.SipSessionImpl mSession; private int mMinInterval; private int mMaxInterval; private int mInterval; private int mPassCount; public SipKeepAliveProcessCallback(SipProfile localProfile, int minInterval, int maxInterval) { mMaxInterval = maxInterval; mMinInterval = minInterval; mLocalProfile = localProfile; } public void start() { synchronized (SipService.this) { if (mSession != null) { return; } mInterval = (mMaxInterval + mMinInterval) / 2; mPassCount = 0; // Don't start measurement if the interval is too small if (mInterval < DEFAULT_KEEPALIVE_INTERVAL || checkTermination()) { if (SKAI_DBG) log("start: measurement aborted; interval=[" + mMinInterval + "," + mMaxInterval + "]"); return; } try { if (SKAI_DBG) log("start: interval=" + mInterval); mGroup = new SipSessionGroupExt(mLocalProfile, null, null); // TODO: remove this line once SipWakeupTimer can better handle // variety of timeout values mGroup.setWakeupTimer(new SipWakeupTimer(mContext, mExecutor)); mSession = (SipSessionGroup.SipSessionImpl) mGroup.createSession(null); mSession.startKeepAliveProcess(mInterval, this); } catch (Throwable t) { onError(SipErrorCode.CLIENT_ERROR, t.toString()); } } } public void stop() { synchronized (SipService.this) { if (mSession != null) { mSession.stopKeepAliveProcess(); mSession = null; } if (mGroup != null) { mGroup.close(); mGroup = null; } mTimer.cancel(this); if (SKAI_DBG) log("stop"); } } private void restart() { synchronized (SipService.this) { // Return immediately if the measurement process is stopped if (mSession == null) return; if (SKAI_DBG) log("restart: interval=" + mInterval); try { mSession.stopKeepAliveProcess(); mPassCount = 0; mSession.startKeepAliveProcess(mInterval, this); } catch (SipException e) { loge("restart", e); } } } private boolean checkTermination() { return ((mMaxInterval - mMinInterval) < MIN_INTERVAL); } // SipSessionGroup.KeepAliveProcessCallback @Override public void onResponse(boolean portChanged) { synchronized (SipService.this) { if (!portChanged) { if (++mPassCount != PASS_THRESHOLD) return; // update the interval, since the current interval is good to // keep the port mapping. if (mKeepAliveInterval > 0) { mLastGoodKeepAliveInterval = mKeepAliveInterval; } mKeepAliveInterval = mMinInterval = mInterval; if (SKAI_DBG) { log("onResponse: portChanged=" + portChanged + " mKeepAliveInterval=" + mKeepAliveInterval); } onKeepAliveIntervalChanged(); } else { // Since the rport is changed, shorten the interval. mMaxInterval = mInterval; } if (checkTermination()) { // update mKeepAliveInterval and stop measurement. stop(); // If all the measurements failed, we still set it to // mMinInterval; If mMinInterval still doesn't work, a new // measurement with min interval=DEFAULT_KEEPALIVE_INTERVAL // will be conducted. mKeepAliveInterval = mMinInterval; if (SKAI_DBG) { log("onResponse: checkTermination mKeepAliveInterval=" + mKeepAliveInterval); } } else { // calculate the new interval and continue. mInterval = (mMaxInterval + mMinInterval) / 2; if (SKAI_DBG) { log("onResponse: mKeepAliveInterval=" + mKeepAliveInterval + ", new mInterval=" + mInterval); } restart(); } } } // SipSessionGroup.KeepAliveProcessCallback @Override public void onError(int errorCode, String description) { if (SKAI_DBG) loge("onError: errorCode=" + errorCode + " desc=" + description); restartLater(); } // timeout handler @Override public void run() { mTimer.cancel(this); restart(); } private void restartLater() { synchronized (SipService.this) { int interval = NAT_MEASUREMENT_RETRY_INTERVAL; mTimer.cancel(this); mTimer.set(interval * 1000, this); } } private void log(String s) { Rlog.d(SKAI_TAG, s); } private void loge(String s) { Rlog.d(SKAI_TAG, s); } private void loge(String s, Throwable t) { Rlog.d(SKAI_TAG, s, t); } } private class SipAutoReg extends SipSessionAdapter implements Runnable, SipSessionGroup.KeepAliveProcessCallback { private String SAR_TAG; private static final boolean SAR_DBG = true; private static final int MIN_KEEPALIVE_SUCCESS_COUNT = 10; private SipSessionGroup.SipSessionImpl mSession; private SipSessionGroup.SipSessionImpl mKeepAliveSession; private SipSessionListenerProxy mProxy = new SipSessionListenerProxy(); private int mBackoff = 1; private boolean mRegistered; private long mExpiryTime; private int mErrorCode; private String mErrorMessage; private boolean mRunning = false; private int mKeepAliveSuccessCount = 0; public void start(SipSessionGroup group) { if (!mRunning) { mRunning = true; 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 mMyWakeLock.acquire(mSession); mSession.unregister(); SAR_TAG = "SipAutoReg:" + obfuscateSipUri(mSession.getLocalProfile().getUriString()); if (SAR_DBG) log("start: group=" + group); } } private void startKeepAliveProcess(int interval) { if (SAR_DBG) log("startKeepAliveProcess: interval=" + interval); if (mKeepAliveSession == null) { mKeepAliveSession = mSession.duplicate(); } else { mKeepAliveSession.stopKeepAliveProcess(); } try { mKeepAliveSession.startKeepAliveProcess(interval, this); } catch (SipException e) { loge("startKeepAliveProcess: interval=" + interval, e); } } private void stopKeepAliveProcess() { if (mKeepAliveSession != null) { mKeepAliveSession.stopKeepAliveProcess(); mKeepAliveSession = null; } mKeepAliveSuccessCount = 0; } // SipSessionGroup.KeepAliveProcessCallback @Override public void onResponse(boolean portChanged) { synchronized (SipService.this) { if (portChanged) { int interval = getKeepAliveInterval(); if (mKeepAliveSuccessCount < MIN_KEEPALIVE_SUCCESS_COUNT) { if (SAR_DBG) { log("onResponse: keepalive doesn't work with interval " + interval + ", past success count=" + mKeepAliveSuccessCount); } if (interval > DEFAULT_KEEPALIVE_INTERVAL) { restartPortMappingLifetimeMeasurement( mSession.getLocalProfile(), interval); mKeepAliveSuccessCount = 0; } } else { if (SAR_DBG) { log("keep keepalive going with interval " + interval + ", past success count=" + mKeepAliveSuccessCount); } mKeepAliveSuccessCount /= 2; } } else { // Start keep-alive interval measurement on the first // successfully kept-alive SipSessionGroup startPortMappingLifetimeMeasurement( mSession.getLocalProfile()); mKeepAliveSuccessCount++; } if (!mRunning || !portChanged) return; // The keep alive process is stopped when port is changed; // Nullify the session so that the process can be restarted // again when the re-registration is done mKeepAliveSession = null; // Acquire wake lock for the registration process. The // lock will be released when registration is complete. mMyWakeLock.acquire(mSession); mSession.register(EXPIRY_TIME); } } // SipSessionGroup.KeepAliveProcessCallback @Override public void onError(int errorCode, String description) { if (SAR_DBG) { loge("onError: errorCode=" + errorCode + " desc=" + description); } onResponse(true); // re-register immediately } public void stop() { if (!mRunning) return; mRunning = false; mMyWakeLock.release(mSession); if (mSession != null) { mSession.setListener(null); if (mNetworkType != -1 && mRegistered) mSession.unregister(); } mTimer.cancel(this); stopKeepAliveProcess(); mRegistered = false; setListener(mProxy.getListener()); } public void onKeepAliveIntervalChanged() { if (mKeepAliveSession != null) { int newInterval = getKeepAliveInterval(); if (SAR_DBG) { log("onKeepAliveIntervalChanged: interval=" + newInterval); } mKeepAliveSuccessCount = 0; startKeepAliveProcess(newInterval); } } public void setListener(ISipSessionListener listener) { synchronized (SipService.this) { mProxy.setListener(listener); 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); } } else if (mNetworkType == -1) { mProxy.onRegistrationFailed(mSession, SipErrorCode.DATA_CONNECTION_LOST, "no data connection"); } else if (!mRunning) { mProxy.onRegistrationFailed(mSession, SipErrorCode.CLIENT_ERROR, "registration not running"); } else { mProxy.onRegistrationFailed(mSession, SipErrorCode.IN_PROGRESS, String.valueOf(state)); } } catch (Throwable t) { loge("setListener: ", t); } } } public boolean isRegistered() { return mRegistered; } // timeout handler: re-register @Override public void run() { synchronized (SipService.this) { if (!mRunning) return; mErrorCode = SipErrorCode.NO_ERROR; mErrorMessage = null; if (SAR_DBG) log("run: registering"); if (mNetworkType != -1) { mMyWakeLock.acquire(mSession); mSession.register(EXPIRY_TIME); } } } private void restart(int duration) { if (SAR_DBG) log("restart: duration=" + 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 (SAR_DBG) log("onRegistering: " + session); synchronized (SipService.this) { if (notCurrentSession(session)) return; mRegistered = false; mProxy.onRegistering(session); } } private boolean notCurrentSession(ISipSession session) { if (session != mSession) { ((SipSessionGroup.SipSessionImpl) session).setListener(null); mMyWakeLock.release(session); return true; } return !mRunning; } @Override public void onRegistrationDone(ISipSession session, int duration) { if (SAR_DBG) log("onRegistrationDone: " + session); synchronized (SipService.this) { if (notCurrentSession(session)) return; mProxy.onRegistrationDone(session, duration); if (duration > 0) { 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); SipProfile localProfile = mSession.getLocalProfile(); if ((mKeepAliveSession == null) && (isBehindNAT(mLocalIp) || localProfile.getSendKeepAlive())) { startKeepAliveProcess(getKeepAliveInterval()); } } mMyWakeLock.release(session); } else { mRegistered = false; mExpiryTime = -1L; if (SAR_DBG) log("Refresh registration immediately"); run(); } } } @Override public void onRegistrationFailed(ISipSession session, int errorCode, String message) { if (SAR_DBG) log("onRegistrationFailed: " + session + ": " + SipErrorCode.toString(errorCode) + ": " + message); synchronized (SipService.this) { if (notCurrentSession(session)) return; switch (errorCode) { case SipErrorCode.INVALID_CREDENTIALS: case SipErrorCode.SERVER_UNREACHABLE: if (SAR_DBG) log(" pause auto-registration"); stop(); break; default: restartLater(); } mErrorCode = errorCode; mErrorMessage = message; mProxy.onRegistrationFailed(session, errorCode, message); mMyWakeLock.release(session); } } @Override public void onRegistrationTimeout(ISipSession session) { if (SAR_DBG) log("onRegistrationTimeout: " + session); synchronized (SipService.this) { if (notCurrentSession(session)) return; mErrorCode = SipErrorCode.TIME_OUT; mProxy.onRegistrationTimeout(session); restartLater(); mMyWakeLock.release(session); } } private void restartLater() { if (SAR_DBG) loge("restartLater"); mRegistered = false; restart(backoffDuration()); } private void log(String s) { Rlog.d(SAR_TAG, s); } private void loge(String s) { Rlog.e(SAR_TAG, s); } private void loge(String s, Throwable e) { Rlog.e(SAR_TAG, s, e); } } private class ConnectivityReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Bundle bundle = intent.getExtras(); if (bundle != null) { final NetworkInfo info = (NetworkInfo) bundle.get(ConnectivityManager.EXTRA_NETWORK_INFO); // Run the handler in MyExecutor to be protected by wake lock mExecutor.execute(new Runnable() { @Override public void run() { onConnectivityChanged(info); } }); } } } private void registerReceivers() { mContext.registerReceiver(mConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); if (DBG) log("registerReceivers:"); } private void unregisterReceivers() { mContext.unregisterReceiver(mConnectivityReceiver); if (DBG) log("unregisterReceivers:"); // Reset variables maintained by ConnectivityReceiver. mWifiLock.release(); mNetworkType = -1; } private void updateWakeLocks() { for (SipSessionGroupExt group : mSipGroups.values()) { if (group.isOpenedToReceiveCalls()) { // Also grab the WifiLock when we are disconnected, so the // system will keep trying to reconnect. It will be released // when the system eventually connects to something else. if (mNetworkType == ConnectivityManager.TYPE_WIFI || mNetworkType == -1) { mWifiLock.acquire(); } else { mWifiLock.release(); } return; } } mWifiLock.release(); mMyWakeLock.reset(); // in case there's a leak } private synchronized void onConnectivityChanged(NetworkInfo info) { // We only care about the default network, and getActiveNetworkInfo() // is the only way to distinguish them. However, as broadcasts are // delivered asynchronously, we might miss DISCONNECTED events from // getActiveNetworkInfo(), which is critical to our SIP stack. To // solve this, if it is a DISCONNECTED event to our current network, // respect it. Otherwise get a new one from getActiveNetworkInfo(). if (info == null || info.isConnected() || info.getType() != mNetworkType) { ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); info = cm.getActiveNetworkInfo(); } // Some devices limit SIP on Wi-Fi. In this case, if we are not on // Wi-Fi, treat it as a DISCONNECTED event. int networkType = (info != null && info.isConnected()) ? info.getType() : -1; if (mSipOnWifiOnly && networkType != ConnectivityManager.TYPE_WIFI) { networkType = -1; } // Ignore the event if the current active network is not changed. if (mNetworkType == networkType) { // TODO: Maybe we need to send seq/generation number return; } if (DBG) { log("onConnectivityChanged: " + mNetworkType + " -> " + networkType); } try { if (mNetworkType != -1) { mLocalIp = null; stopPortMappingMeasurement(); for (SipSessionGroupExt group : mSipGroups.values()) { group.onConnectivityChanged(false); } } mNetworkType = networkType; if (mNetworkType != -1) { mLocalIp = determineLocalIp(); mKeepAliveInterval = -1; mLastGoodKeepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL; for (SipSessionGroupExt group : mSipGroups.values()) { group.onConnectivityChanged(true); } } updateWakeLocks(); } catch (SipException e) { loge("onConnectivityChanged()", e); } } private static Looper createLooper() { HandlerThread thread = new HandlerThread("SipService.Executor"); thread.start(); return thread.getLooper(); } // Executes immediate tasks in a single thread. // Hold/release wake lock for running tasks private class MyExecutor extends Handler implements Executor { MyExecutor() { super(createLooper()); } @Override public void execute(Runnable task) { mMyWakeLock.acquire(task); Message.obtain(this, 0/* don't care */, task).sendToTarget(); } @Override public void handleMessage(Message msg) { if (msg.obj instanceof Runnable) { executeInternal((Runnable) msg.obj); } else { if (DBG) log("handleMessage: not Runnable ignore msg=" + msg); } } private void executeInternal(Runnable task) { try { task.run(); } catch (Throwable t) { loge("run task: " + task, t); } finally { mMyWakeLock.release(task); } } } private void log(String s) { Rlog.d(TAG, s); } private static void slog(String s) { Rlog.d(TAG, s); } private void loge(String s, Throwable e) { Rlog.e(TAG, s, e); } public static String obfuscateSipUri(String sipUri) { StringBuilder sb = new StringBuilder(); int start = 0; sipUri = sipUri.trim(); if (sipUri.startsWith("sip:")) { start = 4; sb.append("sip:"); } char prevC = '\0'; int len = sipUri.length(); for (int i = start; i < len; i++) { char c = sipUri.charAt(i); char nextC = (i + 1 < len) ? sipUri.charAt(i + 1) : '\0'; char charToAppend = '*'; // This logic allows the first and last letter before an '@' sign to show up without // obfuscation as well as the first and last letter an '@' sign. // e.g.: brad@comment.it => b**d@c******.*t if ((i - start < 1) || (i + 1 == len) || isAllowedCharacter(c) || (prevC == '@') || (nextC == '@')) { charToAppend = c; } sb.append(charToAppend); prevC = c; } return sb.toString(); } private static boolean isAllowedCharacter(char c) { return c == '@' || c == '.'; } }