/**
* Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr)
* This file is part of CSipSimple.
*
* CSipSimple is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* If you own a pjsip commercial license you can also redistribute it
* and/or modify it under the terms of the GNU Lesser General Public License
* as an android library.
*
* CSipSimple is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CSipSimple. If not, see <http://www.gnu.org/licenses/>.
*/
package com.csipsimple.service.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.text.TextUtils;
import com.csipsimple.api.SipConfigManager;
import com.csipsimple.api.SipManager;
import com.csipsimple.api.SipProfile;
import com.csipsimple.service.SipService;
import com.csipsimple.service.SipService.SameThreadException;
import com.csipsimple.service.SipService.SipRunnable;
import com.csipsimple.utils.Log;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
public class DynamicReceiver4 extends BroadcastReceiver {
private static final String THIS_FILE = "DynamicReceiver";
// Comes from android.net.vpn.VpnManager.java
// Action for broadcasting a connectivity state.
public static final String ACTION_VPN_CONNECTIVITY = "vpn.connectivity";
/** Key to the connectivity state of a connectivity broadcast event. */
public static final String BROADCAST_CONNECTION_STATE = "connection_state";
private SipService service;
// Store current state
private String mNetworkType;
private boolean mConnected = false;
private String mRoutes = "";
private boolean hasStartedWifi = false;
private Timer pollingTimer;
/**
* Check if the intent received is a sticky broadcast one
* A compat way
* @param it intent received
* @return true if it's an initial sticky broadcast
*/
public boolean compatIsInitialStickyBroadcast(Intent it) {
if(ConnectivityManager.CONNECTIVITY_ACTION.equals(it.getAction())) {
if(!hasStartedWifi) {
hasStartedWifi = true;
return true;
}
}
return false;
}
public DynamicReceiver4(SipService aService) {
service = aService;
}
@Override
public void onReceive(final Context context, final Intent intent) {
// Run the handler in SipServiceExecutor to be protected by wake lock
service.getExecutor().execute(new SipRunnable() {
public void doRun() throws SameThreadException {
onReceiveInternal(context, intent, compatIsInitialStickyBroadcast(intent));
}
});
}
/**
* Internal receiver that will run on sip executor thread
* @param context Application context
* @param intent Intent received
* @throws SameThreadException
*/
private void onReceiveInternal(Context context, Intent intent, boolean isSticky) throws SameThreadException {
String action = intent.getAction();
Log.d(THIS_FILE, "Internal receive " + action);
if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
ConnectivityManager cm =
(ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
onConnectivityChanged(activeNetwork, isSticky);
} else if (action.equals(SipManager.ACTION_SIP_ACCOUNT_CHANGED)) {
final long accountId = intent.getLongExtra(SipProfile.FIELD_ID, SipProfile.INVALID_ID);
// Should that be threaded?
if (accountId != SipProfile.INVALID_ID) {
final SipProfile account = service.getAccount(accountId);
if (account != null) {
Log.d(THIS_FILE, "Enqueue set account registration");
service.setAccountRegistration(account, account.active ? 1 : 0, true);
}
}
} else if (action.equals(SipManager.ACTION_SIP_ACCOUNT_DELETED)){
final long accountId = intent.getLongExtra(SipProfile.FIELD_ID, SipProfile.INVALID_ID);
if(accountId != SipProfile.INVALID_ID) {
final SipProfile fakeProfile = new SipProfile();
fakeProfile.id = accountId;
service.setAccountRegistration(fakeProfile, 0, true);
}
} else if (action.equals(SipManager.ACTION_SIP_CAN_BE_STOPPED)) {
service.cleanStop();
} else if (action.equals(SipManager.ACTION_SIP_REQUEST_RESTART)){
service.restartSipStack();
} else if(action.equals(ACTION_VPN_CONNECTIVITY)) {
onConnectivityChanged(null, isSticky);
}
}
private static final String PROC_NET_ROUTE = "/proc/net/route";
private String dumpRoutes() {
String routes = "";
FileReader fr = null;
try {
fr = new FileReader(PROC_NET_ROUTE);
if(fr != null) {
StringBuffer contentBuf = new StringBuffer();
BufferedReader buf = new BufferedReader(fr);
String line;
while ((line = buf.readLine()) != null) {
contentBuf.append(line+"\n");
}
routes = contentBuf.toString();
buf.close();
}
} catch (FileNotFoundException e) {
Log.e(THIS_FILE, "No route file found routes", e);
} catch (IOException e) {
Log.e(THIS_FILE, "Unable to read route file", e);
}finally {
try {
fr.close();
} catch (IOException e) {
Log.e(THIS_FILE, "Unable to close route file", e);
}
}
// Clean routes that point unique host
// this aims to workaround the fact android 4.x wakeup 3G layer when position is retrieve to resolve over 3g position
String finalRoutes = routes;
if(!TextUtils.isEmpty(routes)) {
String[] items = routes.split("\n");
List<String> finalItems = new ArrayList<String>();
int line = 0;
for(String item : items) {
boolean addItem = true;
if(line > 0){
String[] ent = item.split("\t");
if(ent.length > 8) {
String maskStr = ent[7];
if(maskStr.matches("^[0-9A-F]{8}$")) {
int lastMaskPart = Integer.parseInt(maskStr.substring(0, 2), 16);
if(lastMaskPart > 192) {
// if more than 255.255.255.192 : ignore this line
addItem = false;
}
}else {
Log.w(THIS_FILE, "The route mask does not looks like a mask" + maskStr);
}
}
}
if(addItem) {
finalItems.add(item);
}
line ++;
}
finalRoutes = TextUtils.join("\n", finalItems);
}
return finalRoutes;
}
/**
* Treat the fact that the connectivity has changed
* @param info Network info
* @param incomingOnly start only if for outgoing
* @throws SameThreadException
*/
private void onConnectivityChanged(NetworkInfo info, boolean isSticky) throws SameThreadException {
// 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.getTypeName().equals(mNetworkType)) {
ConnectivityManager cm = (ConnectivityManager) service.getSystemService(Context.CONNECTIVITY_SERVICE);
info = cm.getActiveNetworkInfo();
}
boolean connected = (info != null && info.isConnected() && service.isConnectivityValid());
String networkType = connected ? info.getTypeName() : "null";
String currentRoutes = dumpRoutes();
String oldRoutes;
synchronized (mRoutes) {
oldRoutes = mRoutes;
}
// Ignore the event if the current active network is not changed.
if (connected == mConnected && networkType.equals(mNetworkType) && currentRoutes.equals(oldRoutes)) {
return;
}
if(Log.getLogLevel() >= 4) {
if(!networkType.equals(mNetworkType)) {
Log.d(THIS_FILE, "onConnectivityChanged(): " + mNetworkType +
" -> " + networkType);
}else {
Log.d(THIS_FILE, "Route changed : "+ mRoutes+" -> "+currentRoutes);
}
}
// Now process the event
synchronized (mRoutes) {
mRoutes = currentRoutes;
}
mConnected = connected;
mNetworkType = networkType;
if(!isSticky) {
if (connected) {
service.restartSipStack();
} else {
Log.d(THIS_FILE, "We are not connected, stop");
if(service.stopSipStack()) {
service.stopSelf();
}
}
}
}
public void startMonitoring() {
int pollingIntervalMin = service.getPrefs().getPreferenceIntegerValue(SipConfigManager.NETWORK_ROUTES_POLLING);
Log.d(THIS_FILE, "Start monitoring of route file ? " + pollingIntervalMin);
if(pollingIntervalMin > 0) {
pollingTimer = new Timer("RouteChangeMonitor", true);
pollingTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
String currentRoutes = dumpRoutes();
String oldRoutes;
synchronized (mRoutes) {
oldRoutes = mRoutes;
}
if(!currentRoutes.equalsIgnoreCase(oldRoutes)) {
Log.d(THIS_FILE, "Route changed");
// Run the handler in SipServiceExecutor to be protected by wake lock
service.getExecutor().execute(new SipRunnable() {
public void doRun() throws SameThreadException {
onConnectivityChanged(null, false);
}
});
}
}
}, new Date(), pollingIntervalMin * 60 * 1000);
}
}
public void stopMonitoring() {
if(pollingTimer != null) {
pollingTimer.cancel();
pollingTimer.purge();
pollingTimer = null;
}
}
}