| /* |
| * 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<String, SipSessionGroupExt> mSipGroups = |
| new HashMap<String, SipSessionGroupExt>(); |
| |
| // session ID --> session |
| private Map<String, ISipSession> mPendingSessions = |
| new HashMap<String, ISipSession>(); |
| |
| 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<SipProfile> profiles = new ArrayList<SipProfile>(); |
| 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<String, ISipSession>[] entries = |
| mPendingSessions.entrySet().toArray( |
| new Map.Entry[mPendingSessions.size()]); |
| for (Map.Entry<String, ISipSession> 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.: [email protected] => 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 == '.'; |
| } |
| } |