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