1159 lines
42 KiB
Java
1159 lines
42 KiB
Java
/*
|
|
* Copyright (C) 2010, The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.server.sip;
|
|
|
|
import android.app.AlarmManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.net.ConnectivityManager;
|
|
import android.net.NetworkInfo;
|
|
import android.net.sip.ISipService;
|
|
import android.net.sip.ISipSession;
|
|
import android.net.sip.ISipSessionListener;
|
|
import android.net.sip.SipErrorCode;
|
|
import android.net.sip.SipManager;
|
|
import android.net.sip.SipProfile;
|
|
import android.net.sip.SipSessionAdapter;
|
|
import android.net.sip.SipSessionState;
|
|
import android.net.wifi.WifiManager;
|
|
import android.os.Binder;
|
|
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;
|
|
|
|
/**
|
|
* @hide
|
|
*/
|
|
public final class SipService extends ISipService.Stub {
|
|
private static final String TAG = "SipService";
|
|
private static final boolean DEBUG = true;
|
|
private static final boolean DEBUG_TIMER = DEBUG && false;
|
|
private static final int EXPIRY_TIME = 3600;
|
|
private static final int SHORT_EXPIRY_TIME = 10;
|
|
private static final int MIN_EXPIRY_TIME = 60;
|
|
|
|
private Context mContext;
|
|
private String mLocalIp;
|
|
private String mNetworkType;
|
|
private boolean mConnected;
|
|
private WakeupTimer mTimer;
|
|
private WifiManager.WifiLock mWifiLock;
|
|
|
|
// 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;
|
|
|
|
/**
|
|
* Creates a {@code SipService} instance. Returns null if SIP API is not
|
|
* supported.
|
|
*/
|
|
public static SipService create(Context context) {
|
|
return (SipManager.isApiSupported(context) ? new SipService(context)
|
|
: null);
|
|
}
|
|
|
|
private SipService(Context context) {
|
|
if (DEBUG) Log.d(TAG, " service started!");
|
|
mContext = context;
|
|
mConnectivityReceiver = new ConnectivityReceiver();
|
|
context.registerReceiver(mConnectivityReceiver,
|
|
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
|
|
|
|
mTimer = new WakeupTimer(context);
|
|
}
|
|
|
|
public synchronized SipProfile[] getListOfProfiles() {
|
|
SipProfile[] profiles = new SipProfile[mSipGroups.size()];
|
|
int i = 0;
|
|
for (SipSessionGroupExt group : mSipGroups.values()) {
|
|
profiles[i++] = group.getLocalProfile();
|
|
}
|
|
return profiles;
|
|
}
|
|
|
|
public void open(SipProfile localProfile) {
|
|
localProfile.setCallingUid(Binder.getCallingUid());
|
|
if (localProfile.getAutoRegistration()) {
|
|
openToReceiveCalls(localProfile);
|
|
} else {
|
|
openToMakeCalls(localProfile);
|
|
}
|
|
}
|
|
|
|
private void openToMakeCalls(SipProfile localProfile) {
|
|
try {
|
|
createGroup(localProfile);
|
|
} catch (SipException e) {
|
|
Log.e(TAG, "openToMakeCalls()", e);
|
|
// TODO: how to send the exception back
|
|
}
|
|
}
|
|
|
|
private void openToReceiveCalls(SipProfile localProfile) {
|
|
open3(localProfile, SipManager.SIP_INCOMING_CALL_ACTION, null);
|
|
}
|
|
|
|
public synchronized void open3(SipProfile localProfile,
|
|
String incomingCallBroadcastAction, ISipSessionListener listener) {
|
|
localProfile.setCallingUid(Binder.getCallingUid());
|
|
if (TextUtils.isEmpty(incomingCallBroadcastAction)) {
|
|
throw new RuntimeException(
|
|
"empty broadcast action for incoming call");
|
|
}
|
|
if (DEBUG) Log.d(TAG, "open3: " + localProfile.getUriString() + ": "
|
|
+ incomingCallBroadcastAction + ": " + listener);
|
|
try {
|
|
SipSessionGroupExt group = createGroup(localProfile,
|
|
incomingCallBroadcastAction, listener);
|
|
if (localProfile.getAutoRegistration()) {
|
|
group.openToReceiveCalls();
|
|
if (isWifiOn()) grabWifiLock();
|
|
}
|
|
} catch (SipException e) {
|
|
Log.e(TAG, "openToReceiveCalls()", e);
|
|
// TODO: how to send the exception back
|
|
}
|
|
}
|
|
|
|
public synchronized void close(String localProfileUri) {
|
|
SipSessionGroupExt group = mSipGroups.remove(localProfileUri);
|
|
if (group != null) {
|
|
notifyProfileRemoved(group.getLocalProfile());
|
|
group.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) {
|
|
localProfile.setCallingUid(Binder.getCallingUid());
|
|
if (!mConnected) return null;
|
|
try {
|
|
SipSessionGroupExt group = createGroup(localProfile);
|
|
return group.createSession(listener);
|
|
} catch (SipException e) {
|
|
Log.w(TAG, "createSession()", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public synchronized ISipSession getPendingSession(String callId) {
|
|
if (callId == null) return null;
|
|
return mPendingSessions.get(callId);
|
|
}
|
|
|
|
private String determineLocalIp() {
|
|
try {
|
|
DatagramSocket s = new DatagramSocket();
|
|
s.connect(InetAddress.getByName("192.168.1.1"), 80);
|
|
return s.getLocalAddress().getHostAddress();
|
|
} catch (IOException e) {
|
|
Log.w(TAG, "determineLocalIp()", e);
|
|
// dont do anything; there should be a connectivity change going
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private SipSessionGroupExt createGroup(SipProfile localProfile)
|
|
throws SipException {
|
|
String key = localProfile.getUriString();
|
|
SipSessionGroupExt group = mSipGroups.get(key);
|
|
if (group == null) {
|
|
group = new SipSessionGroupExt(localProfile, null, null);
|
|
mSipGroups.put(key, group);
|
|
notifyProfileAdded(localProfile);
|
|
}
|
|
return group;
|
|
}
|
|
|
|
private SipSessionGroupExt createGroup(SipProfile localProfile,
|
|
String incomingCallBroadcastAction, ISipSessionListener listener)
|
|
throws SipException {
|
|
String key = localProfile.getUriString();
|
|
SipSessionGroupExt group = mSipGroups.get(key);
|
|
if (group != null) {
|
|
group.setIncomingCallBroadcastAction(
|
|
incomingCallBroadcastAction);
|
|
group.setListener(listener);
|
|
} else {
|
|
group = new SipSessionGroupExt(localProfile,
|
|
incomingCallBroadcastAction, listener);
|
|
mSipGroups.put(key, group);
|
|
notifyProfileAdded(localProfile);
|
|
}
|
|
return group;
|
|
}
|
|
|
|
private void notifyProfileAdded(SipProfile localProfile) {
|
|
if (DEBUG) Log.d(TAG, "notify: profile added: " + localProfile);
|
|
Intent intent = new Intent(SipManager.SIP_ADD_PHONE_ACTION);
|
|
intent.putExtra(SipManager.LOCAL_URI_KEY, localProfile.getUriString());
|
|
mContext.sendBroadcast(intent);
|
|
}
|
|
|
|
private void notifyProfileRemoved(SipProfile localProfile) {
|
|
if (DEBUG) 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) {
|
|
if (DEBUG) Log.d(TAG, "acquire wifi lock");
|
|
mWifiLock = ((WifiManager)
|
|
mContext.getSystemService(Context.WIFI_SERVICE))
|
|
.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
|
|
mWifiLock.acquire();
|
|
}
|
|
}
|
|
|
|
private void releaseWifiLock() {
|
|
if (mWifiLock != null) {
|
|
if (DEBUG) Log.d(TAG, "release wifi lock");
|
|
mWifiLock.release();
|
|
mWifiLock = null;
|
|
}
|
|
}
|
|
|
|
private boolean isWifiOn() {
|
|
return "WIFI".equalsIgnoreCase(mNetworkType);
|
|
//return (mConnected && "WIFI".equalsIgnoreCase(mNetworkType));
|
|
}
|
|
|
|
private synchronized void onConnectivityChanged(
|
|
String type, boolean connected) {
|
|
if (DEBUG) Log.d(TAG, "onConnectivityChanged(): "
|
|
+ mNetworkType + (mConnected? " CONNECTED" : " DISCONNECTED")
|
|
+ " --> " + type + (connected? " CONNECTED" : " DISCONNECTED"));
|
|
|
|
boolean sameType = type.equals(mNetworkType);
|
|
if (!sameType && !connected) return;
|
|
|
|
boolean wasWifi = "WIFI".equalsIgnoreCase(mNetworkType);
|
|
boolean isWifi = "WIFI".equalsIgnoreCase(type);
|
|
boolean wifiOff = (isWifi && !connected) || (wasWifi && !sameType);
|
|
boolean wifiOn = isWifi && connected;
|
|
if (wifiOff) {
|
|
releaseWifiLock();
|
|
} else if (wifiOn) {
|
|
if (anyOpened()) grabWifiLock();
|
|
}
|
|
|
|
try {
|
|
boolean wasConnected = mConnected;
|
|
mNetworkType = type;
|
|
mConnected = connected;
|
|
|
|
if (wasConnected) {
|
|
mLocalIp = null;
|
|
for (SipSessionGroupExt group : mSipGroups.values()) {
|
|
group.onConnectivityChanged(false);
|
|
}
|
|
}
|
|
|
|
if (connected) {
|
|
mLocalIp = determineLocalIp();
|
|
for (SipSessionGroupExt group : mSipGroups.values()) {
|
|
group.onConnectivityChanged(true);
|
|
}
|
|
}
|
|
|
|
} catch (SipException e) {
|
|
Log.e(TAG, "onConnectivityChanged()", e);
|
|
}
|
|
}
|
|
|
|
private synchronized void addPendingSession(ISipSession session) {
|
|
try {
|
|
mPendingSessions.put(session.getCallId(), session);
|
|
} catch (RemoteException e) {
|
|
// should not happen with a local call
|
|
Log.e(TAG, "addPendingSession()", e);
|
|
}
|
|
}
|
|
|
|
private class SipSessionGroupExt extends SipSessionAdapter {
|
|
private SipSessionGroup mSipGroup;
|
|
private String mIncomingCallBroadcastAction;
|
|
private boolean mOpened;
|
|
|
|
private AutoRegistrationProcess mAutoRegistration =
|
|
new AutoRegistrationProcess();
|
|
|
|
public SipSessionGroupExt(SipProfile localProfile,
|
|
String incomingCallBroadcastAction,
|
|
ISipSessionListener listener) throws SipException {
|
|
String password = localProfile.getPassword();
|
|
SipProfile p = duplicate(localProfile);
|
|
mSipGroup = createSipSessionGroup(mLocalIp, p, password);
|
|
mIncomingCallBroadcastAction = incomingCallBroadcastAction;
|
|
mAutoRegistration.setListener(listener);
|
|
}
|
|
|
|
public SipProfile getLocalProfile() {
|
|
return mSipGroup.getLocalProfile();
|
|
}
|
|
|
|
// network connectivity is tricky because network can be disconnected
|
|
// at any instant so need to deal with exceptions carefully even when
|
|
// you think you are connected
|
|
private SipSessionGroup createSipSessionGroup(String localIp,
|
|
SipProfile localProfile, String password) throws SipException {
|
|
try {
|
|
return new SipSessionGroup(localIp, localProfile, password);
|
|
} catch (IOException e) {
|
|
// network disconnected
|
|
Log.w(TAG, "createSipSessionGroup(): network disconnected?");
|
|
if (localIp != null) {
|
|
return createSipSessionGroup(null, localProfile, password);
|
|
} else {
|
|
// recursive
|
|
Log.wtf(TAG, "impossible!");
|
|
throw new RuntimeException("createSipSessionGroup");
|
|
}
|
|
}
|
|
}
|
|
|
|
private SipProfile duplicate(SipProfile p) {
|
|
try {
|
|
return new SipProfile.Builder(p).setPassword("*").build();
|
|
} catch (Exception e) {
|
|
Log.wtf(TAG, "duplicate()", e);
|
|
throw new RuntimeException("duplicate profile", e);
|
|
}
|
|
}
|
|
|
|
public void setListener(ISipSessionListener listener) {
|
|
mAutoRegistration.setListener(listener);
|
|
}
|
|
|
|
public void setIncomingCallBroadcastAction(String action) {
|
|
mIncomingCallBroadcastAction = action;
|
|
}
|
|
|
|
public void openToReceiveCalls() throws SipException {
|
|
mOpened = true;
|
|
if (mConnected) {
|
|
mSipGroup.openToReceiveCalls(this);
|
|
mAutoRegistration.start(mSipGroup);
|
|
}
|
|
if (DEBUG) Log.d(TAG, " openToReceiveCalls: " + getUri() + ": "
|
|
+ mIncomingCallBroadcastAction);
|
|
}
|
|
|
|
public void onConnectivityChanged(boolean connected)
|
|
throws SipException {
|
|
mSipGroup.onConnectivityChanged();
|
|
if (connected) {
|
|
resetGroup(mLocalIp);
|
|
if (mOpened) openToReceiveCalls();
|
|
} else {
|
|
// close mSipGroup but remember mOpened
|
|
if (DEBUG) Log.d(TAG, " close auto reg temporarily: "
|
|
+ getUri() + ": " + mIncomingCallBroadcastAction);
|
|
mSipGroup.close();
|
|
mAutoRegistration.stop();
|
|
}
|
|
}
|
|
|
|
private void resetGroup(String localIp) throws SipException {
|
|
try {
|
|
mSipGroup.reset(localIp);
|
|
} catch (IOException e) {
|
|
// network disconnected
|
|
Log.w(TAG, "resetGroup(): network disconnected?");
|
|
if (localIp != null) {
|
|
resetGroup(null); // reset w/o local IP
|
|
} else {
|
|
// recursive
|
|
Log.wtf(TAG, "impossible!");
|
|
throw new RuntimeException("resetGroup");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void closeToNotReceiveCalls() {
|
|
mOpened = false;
|
|
mSipGroup.closeToNotReceiveCalls();
|
|
mAutoRegistration.stop();
|
|
if (DEBUG) Log.d(TAG, " close: " + getUri() + ": "
|
|
+ mIncomingCallBroadcastAction);
|
|
}
|
|
|
|
public ISipSession createSession(ISipSessionListener listener) {
|
|
return mSipGroup.createSession(listener);
|
|
}
|
|
|
|
@Override
|
|
public void onRinging(ISipSession session, SipProfile caller,
|
|
String sessionDescription) {
|
|
synchronized (SipService.this) {
|
|
try {
|
|
if (!isRegistered()) {
|
|
session.endCall();
|
|
return;
|
|
}
|
|
|
|
// send out incoming call broadcast
|
|
addPendingSession(session);
|
|
Intent intent = SipManager.createIncomingCallBroadcast(
|
|
mIncomingCallBroadcastAction, session.getCallId(),
|
|
sessionDescription);
|
|
if (DEBUG) Log.d(TAG, " ringing~~ " + getUri() + ": "
|
|
+ caller.getUri() + ": " + session.getCallId()
|
|
+ " " + mIncomingCallBroadcastAction);
|
|
mContext.sendBroadcast(intent);
|
|
} catch (RemoteException e) {
|
|
// should never happen with a local call
|
|
Log.e(TAG, "processCall()", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onError(ISipSession session, String errorClass,
|
|
String message) {
|
|
if (DEBUG) Log.d(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 = 10;
|
|
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();
|
|
if (DEBUG) Log.d(TAG, "~~~ keepalive");
|
|
mTimer.cancel(this);
|
|
session.sendKeepAlive();
|
|
if (session.isReRegisterRequired()) {
|
|
mSession.register(EXPIRY_TIME);
|
|
} else {
|
|
mTimer.set(INTERVAL * 1000, this);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void stop() {
|
|
mTimer.cancel(this);
|
|
}
|
|
}
|
|
|
|
private class AutoRegistrationProcess extends SipSessionAdapter
|
|
implements Runnable {
|
|
private SipSessionGroup.SipSessionImpl mSession;
|
|
private SipSessionListenerProxy mProxy = new SipSessionListenerProxy();
|
|
private KeepAliveProcess mKeepAliveProcess;
|
|
private int mBackoff = 1;
|
|
private boolean mRegistered;
|
|
private long mExpiryTime;
|
|
private SipErrorCode mErrorCode;
|
|
private String mErrorMessage;
|
|
|
|
private String getAction() {
|
|
return toString();
|
|
}
|
|
|
|
public void start(SipSessionGroup group) {
|
|
if (mSession == null) {
|
|
mBackoff = 1;
|
|
mSession = (SipSessionGroup.SipSessionImpl)
|
|
group.createSession(this);
|
|
// return right away if no active network connection.
|
|
if (mSession == null) return;
|
|
|
|
// start unregistration to clear up old registration at server
|
|
// TODO: when rfc5626 is deployed, use reg-id and sip.instance
|
|
// in registration to avoid adding duplicate entries to server
|
|
mSession.unregister();
|
|
if (DEBUG) Log.d(TAG, "start AutoRegistrationProcess for "
|
|
+ mSession.getLocalProfile().getUriString());
|
|
}
|
|
}
|
|
|
|
public void stop() {
|
|
stop(false);
|
|
}
|
|
|
|
private void stopButKeepStates() {
|
|
stop(true);
|
|
}
|
|
|
|
private void stop(boolean keepStates) {
|
|
if (mSession == null) return;
|
|
if (mConnected && mRegistered) mSession.unregister();
|
|
mTimer.cancel(this);
|
|
if (mKeepAliveProcess != null) {
|
|
mKeepAliveProcess.stop();
|
|
mKeepAliveProcess = null;
|
|
}
|
|
if (!keepStates) {
|
|
mSession = null;
|
|
mRegistered = false;
|
|
}
|
|
}
|
|
|
|
private boolean isStopped() {
|
|
return (mSession == null);
|
|
}
|
|
|
|
public void setListener(ISipSessionListener listener) {
|
|
synchronized (SipService.this) {
|
|
mProxy.setListener(listener);
|
|
if (mSession == null) return;
|
|
|
|
try {
|
|
SipSessionState state = (mSession == null)
|
|
? SipSessionState.READY_TO_CALL
|
|
: Enum.valueOf(
|
|
SipSessionState.class, mSession.getState());
|
|
if ((state == SipSessionState.REGISTERING)
|
|
|| (state == SipSessionState.DEREGISTERING)) {
|
|
mProxy.onRegistering(mSession);
|
|
} else if (mRegistered) {
|
|
int duration = (int)
|
|
(mExpiryTime - SystemClock.elapsedRealtime());
|
|
mProxy.onRegistrationDone(mSession, duration);
|
|
} else if (mErrorCode != null) {
|
|
if (mErrorCode == SipErrorCode.TIME_OUT) {
|
|
mProxy.onRegistrationTimeout(mSession);
|
|
} else {
|
|
mProxy.onRegistrationFailed(mSession,
|
|
mErrorCode.toString(), mErrorMessage);
|
|
}
|
|
}
|
|
} catch (Throwable t) {
|
|
Log.w(TAG, "setListener(): " + t);
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean isRegistered() {
|
|
return mRegistered;
|
|
}
|
|
|
|
public void run() {
|
|
mErrorCode = null;
|
|
mErrorMessage = null;
|
|
if (DEBUG) Log.d(TAG, "~~~ registering");
|
|
synchronized (SipService.this) {
|
|
if (mConnected && !isStopped()) mSession.register(EXPIRY_TIME);
|
|
}
|
|
}
|
|
|
|
private boolean isBehindNAT(String address) {
|
|
try {
|
|
byte[] d = InetAddress.getByName(address).getAddress();
|
|
if ((d[0] == 10) ||
|
|
(((0x000000FF & ((int)d[0])) == 172) &&
|
|
((0x000000F0 & ((int)d[1])) == 16)) ||
|
|
(((0x000000FF & ((int)d[0])) == 192) &&
|
|
((0x000000FF & ((int)d[1])) == 168))) {
|
|
return true;
|
|
}
|
|
} catch (UnknownHostException e) {
|
|
Log.e(TAG, "isBehindAT()" + address, e);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void restart(int duration) {
|
|
if (DEBUG) Log.d(TAG, "Refresh registration " + duration + "s later.");
|
|
mTimer.cancel(this);
|
|
mTimer.set(duration * 1000, this);
|
|
}
|
|
|
|
private int backoffDuration() {
|
|
int duration = SHORT_EXPIRY_TIME * mBackoff;
|
|
if (duration > 3600) {
|
|
duration = 3600;
|
|
} else {
|
|
mBackoff *= 2;
|
|
}
|
|
return duration;
|
|
}
|
|
|
|
@Override
|
|
public void onRegistering(ISipSession session) {
|
|
if (DEBUG) Log.d(TAG, "onRegistering(): " + session);
|
|
synchronized (SipService.this) {
|
|
if (!isStopped() && (session != mSession)) return;
|
|
mRegistered = false;
|
|
mProxy.onRegistering(session);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onRegistrationDone(ISipSession session, int duration) {
|
|
if (DEBUG) Log.d(TAG, "onRegistrationDone(): " + session);
|
|
synchronized (SipService.this) {
|
|
if (!isStopped() && (session != mSession)) return;
|
|
|
|
mProxy.onRegistrationDone(session, duration);
|
|
|
|
if (isStopped()) return;
|
|
|
|
if (duration > 0) {
|
|
mSession.clearReRegisterRequired();
|
|
mExpiryTime = SystemClock.elapsedRealtime()
|
|
+ (duration * 1000);
|
|
|
|
if (!mRegistered) {
|
|
mRegistered = true;
|
|
// allow some overlap to avoid call drop during renew
|
|
duration -= MIN_EXPIRY_TIME;
|
|
if (duration < MIN_EXPIRY_TIME) {
|
|
duration = MIN_EXPIRY_TIME;
|
|
}
|
|
restart(duration);
|
|
|
|
if (isBehindNAT(mLocalIp) ||
|
|
mSession.getLocalProfile().getSendKeepAlive()) {
|
|
if (mKeepAliveProcess == null) {
|
|
mKeepAliveProcess =
|
|
new KeepAliveProcess(mSession);
|
|
}
|
|
mKeepAliveProcess.start();
|
|
}
|
|
}
|
|
} else {
|
|
mRegistered = false;
|
|
mExpiryTime = -1L;
|
|
if (DEBUG) Log.d(TAG, "Refresh registration immediately");
|
|
run();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onRegistrationFailed(ISipSession session,
|
|
String errorCodeString, String message) {
|
|
SipErrorCode errorCode =
|
|
Enum.valueOf(SipErrorCode.class, errorCodeString);
|
|
if (DEBUG) Log.d(TAG, "onRegistrationFailed(): " + session + ": "
|
|
+ errorCode + ": " + message);
|
|
synchronized (SipService.this) {
|
|
if (!isStopped() && (session != mSession)) return;
|
|
mErrorCode = errorCode;
|
|
mErrorMessage = message;
|
|
mProxy.onRegistrationFailed(session, errorCode.toString(),
|
|
message);
|
|
|
|
if (errorCode == SipErrorCode.INVALID_CREDENTIALS) {
|
|
if (DEBUG) Log.d(TAG, " pause auto-registration");
|
|
stopButKeepStates();
|
|
} else if (!isStopped()) {
|
|
onError();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onRegistrationTimeout(ISipSession session) {
|
|
if (DEBUG) Log.d(TAG, "onRegistrationTimeout(): " + session);
|
|
synchronized (SipService.this) {
|
|
if (!isStopped() && (session != mSession)) return;
|
|
mErrorCode = SipErrorCode.TIME_OUT;
|
|
mProxy.onRegistrationTimeout(session);
|
|
|
|
if (!isStopped()) {
|
|
mRegistered = false;
|
|
onError();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void onError() {
|
|
mRegistered = false;
|
|
restart(backoffDuration());
|
|
if (mKeepAliveProcess != null) {
|
|
mKeepAliveProcess.stop();
|
|
mKeepAliveProcess = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private class ConnectivityReceiver extends BroadcastReceiver {
|
|
private Timer mTimer = new Timer();
|
|
private MyTimerTask mTask;
|
|
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
String action = intent.getAction();
|
|
if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
|
|
Bundle b = intent.getExtras();
|
|
if (b != null) {
|
|
NetworkInfo netInfo = (NetworkInfo)
|
|
b.get(ConnectivityManager.EXTRA_NETWORK_INFO);
|
|
String type = netInfo.getTypeName();
|
|
NetworkInfo.State state = netInfo.getState();
|
|
|
|
NetworkInfo activeNetInfo = getActiveNetworkInfo();
|
|
if (DEBUG) {
|
|
if (activeNetInfo != null) {
|
|
Log.d(TAG, "active network: "
|
|
+ activeNetInfo.getTypeName()
|
|
+ ((activeNetInfo.getState() == NetworkInfo.State.CONNECTED)
|
|
? " CONNECTED" : " DISCONNECTED"));
|
|
} else {
|
|
Log.d(TAG, "active network: null");
|
|
}
|
|
}
|
|
if ((state == NetworkInfo.State.CONNECTED)
|
|
&& (activeNetInfo != null)
|
|
&& (activeNetInfo.getType() != netInfo.getType())) {
|
|
if (DEBUG) Log.d(TAG, "ignore connect event: " + type
|
|
+ ", active: " + activeNetInfo.getTypeName());
|
|
return;
|
|
}
|
|
|
|
if (state == NetworkInfo.State.CONNECTED) {
|
|
if (DEBUG) Log.d(TAG, "Connectivity alert: CONNECTED " + type);
|
|
onChanged(type, true);
|
|
} else if (state == NetworkInfo.State.DISCONNECTED) {
|
|
if (DEBUG) Log.d(TAG, "Connectivity alert: DISCONNECTED " + type);
|
|
onChanged(type, false);
|
|
} else {
|
|
if (DEBUG) Log.d(TAG, "Connectivity alert not processed: "
|
|
+ state + " " + type);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private NetworkInfo getActiveNetworkInfo() {
|
|
ConnectivityManager cm = (ConnectivityManager)
|
|
mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
return cm.getActiveNetworkInfo();
|
|
}
|
|
|
|
private void onChanged(String type, boolean connected) {
|
|
synchronized (SipService.this) {
|
|
// When turning on WIFI, it needs some time for network
|
|
// connectivity to get stabile so we defer good news (because
|
|
// we want to skip the interim ones) but deliver bad news
|
|
// immediately
|
|
if (connected) {
|
|
if (mTask != null) mTask.cancel();
|
|
mTask = new MyTimerTask(type, connected);
|
|
mTimer.schedule(mTask, 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;
|
|
if (DEBUG) Log.d(TAG, " deliver change for " + mNetworkType
|
|
+ (mConnected ? " CONNECTED" : "DISCONNECTED"));
|
|
onConnectivityChanged(mNetworkType, mConnected);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: clean up pending SipSession(s) periodically
|
|
|
|
|
|
/**
|
|
* Timer that can schedule events to occur even when the device is in sleep.
|
|
* Only used internally in this package.
|
|
*/
|
|
class WakeupTimer extends BroadcastReceiver {
|
|
private static final String TAG = "_SIP.WkTimer_";
|
|
private static final String TRIGGER_TIME = "TriggerTime";
|
|
|
|
private Context mContext;
|
|
private AlarmManager mAlarmManager;
|
|
|
|
// runnable --> time to execute in SystemClock
|
|
private TreeSet<MyEvent> mEventQueue =
|
|
new TreeSet<MyEvent>(new MyEventComparator());
|
|
|
|
private PendingIntent mPendingIntent;
|
|
|
|
public WakeupTimer(Context context) {
|
|
mContext = context;
|
|
mAlarmManager = (AlarmManager)
|
|
context.getSystemService(Context.ALARM_SERVICE);
|
|
|
|
IntentFilter filter = new IntentFilter(getAction());
|
|
context.registerReceiver(this, filter);
|
|
}
|
|
|
|
/**
|
|
* Stops the timer. No event can be scheduled after this method is called.
|
|
*/
|
|
public synchronized void stop() {
|
|
mContext.unregisterReceiver(this);
|
|
if (mPendingIntent != null) {
|
|
mAlarmManager.cancel(mPendingIntent);
|
|
mPendingIntent = null;
|
|
}
|
|
mEventQueue.clear();
|
|
mEventQueue = null;
|
|
}
|
|
|
|
private synchronized boolean stopped() {
|
|
if (mEventQueue == null) {
|
|
Log.w(TAG, "Timer stopped");
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void cancelAlarm() {
|
|
mAlarmManager.cancel(mPendingIntent);
|
|
mPendingIntent = null;
|
|
}
|
|
|
|
private void recalculatePeriods() {
|
|
if (mEventQueue.isEmpty()) return;
|
|
|
|
MyEvent firstEvent = mEventQueue.first();
|
|
int minPeriod = firstEvent.mMaxPeriod;
|
|
long minTriggerTime = firstEvent.mTriggerTime;
|
|
for (MyEvent e : mEventQueue) {
|
|
e.mPeriod = e.mMaxPeriod / minPeriod * minPeriod;
|
|
int interval = (int) (e.mLastTriggerTime + e.mMaxPeriod
|
|
- minTriggerTime);
|
|
interval = interval / minPeriod * minPeriod;
|
|
e.mTriggerTime = minTriggerTime + interval;
|
|
}
|
|
TreeSet<MyEvent> newQueue = new TreeSet<MyEvent>(
|
|
mEventQueue.comparator());
|
|
newQueue.addAll((Collection<MyEvent>) mEventQueue);
|
|
mEventQueue.clear();
|
|
mEventQueue = newQueue;
|
|
if (DEBUG_TIMER) {
|
|
Log.d(TAG, "queue re-calculated");
|
|
printQueue();
|
|
}
|
|
}
|
|
|
|
// Determines the period and the trigger time of the new event and insert it
|
|
// to the queue.
|
|
private void insertEvent(MyEvent event) {
|
|
long now = SystemClock.elapsedRealtime();
|
|
if (mEventQueue.isEmpty()) {
|
|
event.mTriggerTime = now + event.mPeriod;
|
|
mEventQueue.add(event);
|
|
return;
|
|
}
|
|
MyEvent firstEvent = mEventQueue.first();
|
|
int minPeriod = firstEvent.mPeriod;
|
|
if (minPeriod <= event.mMaxPeriod) {
|
|
event.mPeriod = event.mMaxPeriod / minPeriod * minPeriod;
|
|
int interval = event.mMaxPeriod;
|
|
interval -= (int) (firstEvent.mTriggerTime - now);
|
|
interval = interval / minPeriod * minPeriod;
|
|
event.mTriggerTime = firstEvent.mTriggerTime + interval;
|
|
mEventQueue.add(event);
|
|
} else {
|
|
long triggerTime = now + event.mPeriod;
|
|
if (firstEvent.mTriggerTime < triggerTime) {
|
|
event.mTriggerTime = firstEvent.mTriggerTime;
|
|
event.mLastTriggerTime -= event.mPeriod;
|
|
} else {
|
|
event.mTriggerTime = triggerTime;
|
|
}
|
|
mEventQueue.add(event);
|
|
recalculatePeriods();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a periodic timer.
|
|
*
|
|
* @param period the timer period; in milli-second
|
|
* @param callback is called back when the timer goes off; the same callback
|
|
* can be specified in multiple timer events
|
|
*/
|
|
public synchronized void set(int period, Runnable callback) {
|
|
if (stopped()) return;
|
|
|
|
long now = SystemClock.elapsedRealtime();
|
|
MyEvent event = new MyEvent(period, callback, now);
|
|
insertEvent(event);
|
|
|
|
if (mEventQueue.first() == event) {
|
|
if (mEventQueue.size() > 1) cancelAlarm();
|
|
scheduleNext();
|
|
}
|
|
|
|
long triggerTime = event.mTriggerTime;
|
|
if (DEBUG_TIMER) {
|
|
Log.d(TAG, " add event " + event + " scheduled at "
|
|
+ showTime(triggerTime) + " at " + showTime(now)
|
|
+ ", #events=" + mEventQueue.size());
|
|
printQueue();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancels all the timer events with the specified callback.
|
|
*
|
|
* @param callback the callback
|
|
*/
|
|
public synchronized void cancel(Runnable callback) {
|
|
if (stopped() || mEventQueue.isEmpty()) return;
|
|
if (DEBUG_TIMER) Log.d(TAG, "cancel:" + callback);
|
|
|
|
MyEvent firstEvent = mEventQueue.first();
|
|
for (Iterator<MyEvent> iter = mEventQueue.iterator();
|
|
iter.hasNext();) {
|
|
MyEvent event = iter.next();
|
|
if (event.mCallback == callback) {
|
|
iter.remove();
|
|
if (DEBUG_TIMER) Log.d(TAG, " cancel found:" + event);
|
|
}
|
|
}
|
|
if (mEventQueue.isEmpty()) {
|
|
cancelAlarm();
|
|
} else if (mEventQueue.first() != firstEvent) {
|
|
cancelAlarm();
|
|
firstEvent = mEventQueue.first();
|
|
firstEvent.mPeriod = firstEvent.mMaxPeriod;
|
|
firstEvent.mTriggerTime = firstEvent.mLastTriggerTime
|
|
+ firstEvent.mPeriod;
|
|
recalculatePeriods();
|
|
scheduleNext();
|
|
}
|
|
if (DEBUG_TIMER) {
|
|
Log.d(TAG, "after cancel:");
|
|
printQueue();
|
|
}
|
|
}
|
|
|
|
private void scheduleNext() {
|
|
if (stopped() || mEventQueue.isEmpty()) return;
|
|
|
|
if (mPendingIntent != null) {
|
|
throw new RuntimeException("pendingIntent is not null!");
|
|
}
|
|
|
|
MyEvent event = mEventQueue.first();
|
|
Intent intent = new Intent(getAction());
|
|
intent.putExtra(TRIGGER_TIME, event.mTriggerTime);
|
|
PendingIntent pendingIntent = mPendingIntent =
|
|
PendingIntent.getBroadcast(mContext, 0, intent,
|
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
|
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
|
event.mTriggerTime, pendingIntent);
|
|
}
|
|
|
|
@Override
|
|
public synchronized void onReceive(Context context, Intent intent) {
|
|
String action = intent.getAction();
|
|
if (getAction().equals(action)
|
|
&& intent.getExtras().containsKey(TRIGGER_TIME)) {
|
|
mPendingIntent = null;
|
|
long triggerTime = intent.getLongExtra(TRIGGER_TIME, -1L);
|
|
execute(triggerTime);
|
|
} else {
|
|
Log.d(TAG, "unrecognized intent: " + intent);
|
|
}
|
|
}
|
|
|
|
private void printQueue() {
|
|
int count = 0;
|
|
for (MyEvent event : mEventQueue) {
|
|
Log.d(TAG, " " + event + ": scheduled at "
|
|
+ showTime(event.mTriggerTime) + ": last at "
|
|
+ showTime(event.mLastTriggerTime));
|
|
if (++count >= 5) break;
|
|
}
|
|
if (mEventQueue.size() > count) {
|
|
Log.d(TAG, " .....");
|
|
} else if (count == 0) {
|
|
Log.d(TAG, " <empty>");
|
|
}
|
|
}
|
|
|
|
private void execute(long triggerTime) {
|
|
if (DEBUG_TIMER) Log.d(TAG, "time's up, triggerTime = "
|
|
+ showTime(triggerTime) + ": " + mEventQueue.size());
|
|
if (stopped() || mEventQueue.isEmpty()) return;
|
|
|
|
for (MyEvent event : mEventQueue) {
|
|
if (event.mTriggerTime != triggerTime) break;
|
|
if (DEBUG_TIMER) Log.d(TAG, "execute " + event);
|
|
|
|
event.mLastTriggerTime = event.mTriggerTime;
|
|
event.mTriggerTime += event.mPeriod;
|
|
|
|
// run the callback in a new thread to prevent deadlock
|
|
new Thread(event.mCallback).start();
|
|
}
|
|
if (DEBUG_TIMER) {
|
|
Log.d(TAG, "after timeout execution");
|
|
printQueue();
|
|
}
|
|
scheduleNext();
|
|
}
|
|
|
|
private String getAction() {
|
|
return toString();
|
|
}
|
|
|
|
private String showTime(long time) {
|
|
int ms = (int) (time % 1000);
|
|
int s = (int) (time / 1000);
|
|
int m = s / 60;
|
|
s %= 60;
|
|
return String.format("%d.%d.%d", m, s, ms);
|
|
}
|
|
}
|
|
|
|
private static class MyEvent {
|
|
int mPeriod;
|
|
int mMaxPeriod;
|
|
long mTriggerTime;
|
|
long mLastTriggerTime;
|
|
Runnable mCallback;
|
|
|
|
MyEvent(int period, Runnable callback, long now) {
|
|
mPeriod = mMaxPeriod = period;
|
|
mCallback = callback;
|
|
mLastTriggerTime = now;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
String s = super.toString();
|
|
s = s.substring(s.indexOf("@"));
|
|
return s + ":" + (mPeriod / 1000) + ":" + (mMaxPeriod / 1000) + ":"
|
|
+ toString(mCallback);
|
|
}
|
|
|
|
private String toString(Object o) {
|
|
String s = o.toString();
|
|
int index = s.indexOf("$");
|
|
if (index > 0) s = s.substring(index + 1);
|
|
return s;
|
|
}
|
|
}
|
|
|
|
private static class MyEventComparator implements Comparator<MyEvent> {
|
|
public int compare(MyEvent e1, MyEvent e2) {
|
|
if (e1 == e2) return 0;
|
|
int diff = e1.mMaxPeriod - e2.mMaxPeriod;
|
|
if (diff == 0) diff = -1;
|
|
return diff;
|
|
}
|
|
|
|
public boolean equals(Object that) {
|
|
return (this == that);
|
|
}
|
|
}
|
|
}
|