5
0
mirror of https://github.com/cwinfo/envayasms.git synced 2024-12-04 20:45:32 +00:00

automatic failover between wifi/mobile if server cannot be reached; send timestamp of incoming message to server

This commit is contained in:
Jesse Young 2011-09-29 16:02:37 -07:00
parent 31085128eb
commit 1081f57580
16 changed files with 350 additions and 39 deletions

View File

@ -1,11 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.envaya.sms"
android:versionCode="11"
android:versionName="2.0-rc1">
android:versionCode="12"
android:versionName="2.0-rc2">
<uses-sdk android:minSdkVersion="4" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
@ -78,7 +82,10 @@
</receiver>
<receiver android:name=".receiver.IncomingMessageRetry">
</receiver>
</receiver>
<receiver android:name=".receiver.ReenableWifiReceiver">
</receiver>
<receiver android:name=".receiver.BootReceiver">
<intent-filter>
@ -95,11 +102,17 @@
</intent-filter>
</receiver>
<receiver android:name=".receiver.ConnectivityChangeReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
<service android:name=".CheckMmsInboxService">
</service>
<service android:name=".ForegroundService">
</service>
</service>
</application>
</manifest>

View File

@ -54,6 +54,13 @@
>
</ListPreference>
<CheckBoxPreference
android:key="network_failover"
android:title="Network Failover"
android:summaryOff="Do nothing if phone can't connect to server via Wi-Fi"
android:summaryOn="Automatically switch to mobile data if phone can't connect to server via Wi-Fi"
></CheckBoxPreference>
<CheckBoxPreference
android:key="test_mode"
android:title="Test mode"

View File

@ -183,6 +183,7 @@ class EnvayaSMS_Action_Incoming extends EnvayaSMS_Action
public $message; // The message body of the SMS, or the content of the text/plain part of the MMS.
public $message_type; // EnvayaSMS::MESSAGE_TYPE_MMS or EnvayaSMS::MESSAGE_TYPE_SMS
public $mms_parts; // array of EnvayaSMS_MMS_Part instances
public $timestamp; // timestamp of incoming message (added in version 12)
function __construct($request)
{
@ -191,6 +192,7 @@ class EnvayaSMS_Action_Incoming extends EnvayaSMS_Action
$this->from = $_POST['from'];
$this->message = $_POST['message'];
$this->message_type = $_POST['message_type'];
$this->timestamp = @$_POST['timestamp'];
if ($this->message_type == EnvayaSMS::MESSAGE_TYPE_MMS)
{

View File

@ -10,7 +10,10 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.SystemClock;
import android.preference.PreferenceManager;
@ -18,6 +21,8 @@ import android.telephony.SmsManager;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Comparator;
@ -40,6 +45,7 @@ import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.envaya.sms.receiver.DequeueOutgoingMessageReceiver;
import org.envaya.sms.receiver.OutgoingMessagePoller;
import org.envaya.sms.receiver.ReenableWifiReceiver;
import org.envaya.sms.task.HttpTask;
import org.envaya.sms.task.PollerTask;
import org.json.JSONArray;
@ -94,11 +100,18 @@ public final class App extends Application {
public static final Uri INCOMING_URI = Uri.withAppendedPath(CONTENT_URI, "incoming");
public static final Uri OUTGOING_URI = Uri.withAppendedPath(CONTENT_URI, "outgoing");
// how long we disable wifi when there is no connection to the server
// (should be longer than CONNECTIVITY_FAILOVER_INTERVAL)
public static final int DISABLE_WIFI_INTERVAL = 3600000;
// how often we can automatically failover between wifi/mobile connection
public static final int CONNECTIVITY_FAILOVER_INTERVAL = 1800000;
// max per-app outgoing SMS rate used by com.android.internal.telephony.SMSDispatcher
// with a slightly longer check period to account for variance in the time difference
// between when we prepare messages and when SMSDispatcher receives them
public static int OUTGOING_SMS_CHECK_PERIOD = 3605000; // one hour plus 5 sec (in ms)
public static int OUTGOING_SMS_MAX_COUNT = 100;
public static final int OUTGOING_SMS_CHECK_PERIOD = 3605000; // one hour plus 5 sec (in ms)
public static final int OUTGOING_SMS_MAX_COUNT = 100;
private Map<Uri, IncomingMessage> incomingMessages = new HashMap<Uri, IncomingMessage>();
private Map<Uri, OutgoingMessage> outgoingMessages = new HashMap<Uri, OutgoingMessage>();
@ -417,6 +430,11 @@ public final class App extends Application {
return settings.getBoolean("enabled", false);
}
public boolean isNetworkFailoverEnabled()
{
return settings.getBoolean("network_failover", false);
}
public boolean isTestMode()
{
return settings.getBoolean("test_mode", false);
@ -713,14 +731,16 @@ public final class App extends Application {
public synchronized void retryIncomingMessage(Uri uri) {
IncomingMessage message = incomingMessages.get(uri);
if (message != null) {
if (message != null
&& message.getProcessingState() == IncomingMessage.ProcessingState.Scheduled) {
enqueueIncomingMessage(message);
}
}
public synchronized void retryOutgoingMessage(Uri uri) {
OutgoingMessage sms = outgoingMessages.get(uri);
if (sms != null) {
if (sms != null
&& sms.getProcessingState() == OutgoingMessage.ProcessingState.Scheduled) {
enqueueOutgoingMessage(sms);
}
}
@ -898,4 +918,189 @@ public final class App extends Application {
}
return httpClient;
}
private class ConnectivityCheckState
{
//private int networkType;
private long lastCheckTime; // when we checked connectivity on this network
public ConnectivityCheckState(int networkType)
{
//this.networkType = networkType;
}
public synchronized boolean canCheck()
{
long time = SystemClock.elapsedRealtime();
return (time - lastCheckTime >= App.CONNECTIVITY_FAILOVER_INTERVAL);
}
public void setChecked()
{
lastCheckTime = SystemClock.elapsedRealtime();
}
}
private Map<Integer,ConnectivityCheckState> connectivityCheckStates
= new HashMap<Integer, ConnectivityCheckState>();
private Thread connectivityThread;
/*
* Normally we rely on Android to automatically switch between
* mobile data and Wi-Fi, but if the phone is connected to a Wi-Fi router
* that doesn't have a connection to the internet, Android won't know
* the difference. So we if we can't actually reach the remote host via
* the current connection, we toggle the Wi-Fi radio so that Android
* will switch to the other connection.
*
* If the host is unreachable on both connections, we don't want to
* keep toggling the radio forever, so there is a timeout before we can
* recheck connectivity on a particular connection.
*
* When we disable the Wi-Fi radio, we set a timeout to reenable it after
* a while in hopes that connectivity will be restored.
*/
public synchronized void asyncCheckConnectivity()
{
ConnectivityManager cm =
(ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
if (activeNetwork == null || !activeNetwork.isConnected())
{
WifiManager wmgr = (WifiManager)getSystemService(Context.WIFI_SERVICE);
if (!wmgr.isWifiEnabled() && isNetworkFailoverEnabled())
{
wmgr.setWifiEnabled(true);
}
return;
}
final int networkType = activeNetwork.getType();
ConnectivityCheckState state =
connectivityCheckStates.get(networkType);
if (state == null)
{
state = new ConnectivityCheckState(networkType);
connectivityCheckStates.put(networkType, state);
}
if (!state.canCheck()
|| (connectivityThread != null && connectivityThread.isAlive()))
{
return;
}
state.setChecked();
connectivityThread = new Thread() {
@Override
public void run()
{
Uri serverUrl = Uri.parse(getServerUrl());
String hostName = serverUrl.getHost();
log("Checking connectivity to "+hostName+"...");
try
{
InetAddress addr = InetAddress.getByName(hostName);
if (addr.isReachable(App.HTTP_CONNECTION_TIMEOUT))
{
log("OK");
onConnectivityRestored();
return;
}
}
catch (IOException ex)
{
// just what we suspected...
// server not reachable on this interface
}
log("Can't connect to "+hostName+".");
WifiManager wmgr = (WifiManager)getSystemService(Context.WIFI_SERVICE);
if (!isNetworkFailoverEnabled())
{
log("Network failover disabled.");
}
else if (networkType == ConnectivityManager.TYPE_WIFI)
{
log("Switching from WIFI to MOBILE");
PendingIntent pendingIntent = PendingIntent.getBroadcast(App.this,
0,
new Intent(App.this, ReenableWifiReceiver.class),
0);
// set an alarm to try restoring Wi-Fi in a little while
AlarmManager alarm =
(AlarmManager)getSystemService(Context.ALARM_SERVICE);
alarm.set(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + App.DISABLE_WIFI_INTERVAL,
pendingIntent);
wmgr.setWifiEnabled(false);
}
else if (networkType == ConnectivityManager.TYPE_MOBILE
&& !wmgr.isWifiEnabled())
{
log("Switching from MOBILE to WIFI");
wmgr.setWifiEnabled(true);
}
else
{
log("Can't automatically fix connectivity.");
}
}
};
connectivityThread.start();
}
private int activeNetworkType = -1;
public synchronized void onConnectivityChanged()
{
ConnectivityManager cm =
(ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnected())
{
return;
}
int networkType = networkInfo.getType();
if (networkType == activeNetworkType)
{
return;
}
activeNetworkType = networkType;
log("Connected to " + networkInfo.getTypeName());
asyncCheckConnectivity();
}
public void onConnectivityRestored()
{
retryStuckIncomingMessages();
if (getOutgoingPollSeconds() > 0)
{
checkOutgoingMessages();
}
// failed outgoing message status notifications are dropped...
}
}

View File

@ -1,13 +1,13 @@
package org.envaya.sms;
import android.content.Intent;
import android.net.Uri;
import org.envaya.sms.receiver.IncomingMessageRetry;
public abstract class IncomingMessage extends QueuedMessage {
protected String from;
protected long timestamp; // unix timestamp in milliseconds
private ProcessingState state = ProcessingState.None;
public enum ProcessingState
@ -17,10 +17,16 @@ public abstract class IncomingMessage extends QueuedMessage {
Scheduled // waiting for a while before retrying after failure forwarding
}
public IncomingMessage(App app, String from)
public IncomingMessage(App app, String from, long timestamp)
{
super(app);
this.from = from;
this.timestamp = timestamp;
}
public long getTimestamp()
{
return timestamp;
}
public ProcessingState getProcessingState()

View File

@ -19,9 +19,9 @@ public class IncomingMms extends IncomingMessage {
long id;
String contentLocation;
public IncomingMms(App app, String from, long id)
public IncomingMms(App app, String from, long timestamp, long id)
{
super(app, from);
super(app, from, timestamp);
this.parts = new ArrayList<MmsPart>();
this.id = id;
}
@ -154,8 +154,7 @@ public class IncomingMms extends IncomingMessage {
i++;
}
ForwarderTask task = new ForwarderTask(this,
new BasicNameValuePair("from", getFrom()),
ForwarderTask task = new ForwarderTask(this,
new BasicNameValuePair("message", message),
new BasicNameValuePair("message_type", App.MESSAGE_TYPE_MMS),
new BasicNameValuePair("mms_parts", partsMetadata.toString())

View File

@ -12,11 +12,13 @@ import org.envaya.sms.task.ForwarderTask;
public class IncomingSms extends IncomingMessage {
protected String message;
protected long timestampMillis;
// constructor for SMS retrieved from android.provider.Telephony.SMS_RECEIVED intent
public IncomingSms(App app, List<SmsMessage> smsParts) throws InvalidParameterException {
super(app, smsParts.get(0).getOriginatingAddress());
super(app,
smsParts.get(0).getOriginatingAddress(),
smsParts.get(0).getTimestampMillis()
);
this.message = smsParts.get(0).getMessageBody();
@ -34,15 +36,12 @@ public class IncomingSms extends IncomingMessage {
message = message + smsPart.getMessageBody();
}
this.timestampMillis = smsParts.get(0).getTimestampMillis();
}
// constructor for SMS retrieved from Messaging inbox
public IncomingSms(App app, String from, String message, long timestampMillis) {
super(app, from);
super(app, from, timestampMillis);
this.message = message;
this.timestampMillis = timestampMillis;
}
public String getMessageBody()
@ -60,7 +59,7 @@ public class IncomingSms extends IncomingMessage {
return Uri.withAppendedPath(App.INCOMING_URI,
"sms/" +
Uri.encode(from) + "/"
+ timestampMillis + "/" +
+ timestamp + "/" +
Uri.encode(message));
}
@ -72,7 +71,6 @@ public class IncomingSms extends IncomingMessage {
}
new ForwarderTask(this,
new BasicNameValuePair("from", getFrom()),
new BasicNameValuePair("message_type", App.MESSAGE_TYPE_SMS),
new BasicNameValuePair("message", getMessageBody())
).execute();

View File

@ -107,7 +107,7 @@ public class MmsUtils
String m_type = "" + MESSAGE_TYPE_RETRIEVE_CONF;
Cursor c = contentResolver.query(INBOX_URI,
new String[] {"_id", "ct_l"},
new String[] {"_id", "ct_l", "date"},
"m_type = ? ", new String[] { m_type }, null);
List<IncomingMms> messages = new ArrayList<IncomingMms>();
@ -115,8 +115,13 @@ public class MmsUtils
while (c.moveToNext())
{
long id = c.getLong(0);
long date = c.getLong(2);
IncomingMms mms = new IncomingMms(app, getSenderNumber(id), id);
IncomingMms mms = new IncomingMms(app,
getSenderNumber(id),
date * 1000, // MMS timestamp is in seconds for some reason,
// while everything else is in ms
id);
mms.setContentLocation(c.getString(1));

View File

@ -36,11 +36,11 @@ public abstract class QueuedMessage
int minute = second * 60;
if (numRetries == 1) {
app.log("1st failure; retry in 1 minute");
nextRetryTime = now + 1 * minute;
app.log("1st failure; retry in 20 seconds");
nextRetryTime = now + 20 * second;
} else if (numRetries == 2) {
app.log("2nd failure; retry in 10 minutes");
nextRetryTime = now + 10 * minute;
app.log("2nd failure; retry in 5 minutes");
nextRetryTime = now + 5 * minute;
} else if (numRetries == 3) {
app.log("3rd failure; retry in 1 hour");
nextRetryTime = now + 60 * minute;

View File

@ -0,0 +1,24 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import org.envaya.sms.App;
public class ConnectivityChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final App app = (App) context.getApplicationContext();
if (!app.isEnabled())
{
return;
}
app.onConnectivityChanged();
}
}

View File

@ -10,6 +10,12 @@ public class DequeueOutgoingMessageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
App app = (App) context.getApplicationContext();
if (!app.isEnabled())
{
return;
}
app.maybeDequeueOutgoingMessage();
}
}

View File

@ -12,6 +12,11 @@ public class IncomingMessageRetry extends BroadcastReceiver
public void onReceive(Context context, Intent intent)
{
App app = (App) context.getApplicationContext();
if (!app.isEnabled())
{
return;
}
app.retryIncomingMessage(intent.getData());
}
}

View File

@ -11,7 +11,11 @@ public class OutgoingMessageRetry extends BroadcastReceiver
@Override
public void onReceive(Context context, Intent intent)
{
App app = (App) context.getApplicationContext();
App app = (App) context.getApplicationContext();
if (!app.isEnabled())
{
return;
}
app.retryOutgoingMessage(intent.getData());
}
}

View File

@ -0,0 +1,29 @@
package org.envaya.sms.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.WifiManager;
import org.envaya.sms.App;
public class ReenableWifiReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
App app = (App) context.getApplicationContext();
if (!app.isEnabled())
{
return;
}
WifiManager wmgr =
(WifiManager)app.getSystemService(Context.WIFI_SERVICE);
if (!wmgr.isWifiEnabled())
{
app.log("Reenabling Wi-Fi");
wmgr.setWifiEnabled(true);
}
}
}

View File

@ -8,18 +8,20 @@ import org.envaya.sms.OutgoingMessage;
public class ForwarderTask extends HttpTask {
private IncomingMessage originalSms;
private IncomingMessage message;
public ForwarderTask(IncomingMessage originalSms, BasicNameValuePair... paramsArr) {
super(originalSms.app, paramsArr);
this.originalSms = originalSms;
public ForwarderTask(IncomingMessage message, BasicNameValuePair... paramsArr) {
super(message.app, paramsArr);
this.message = message;
params.add(new BasicNameValuePair("action", App.ACTION_INCOMING));
params.add(new BasicNameValuePair("from", message.getFrom()));
params.add(new BasicNameValuePair("timestamp", "" + message.getTimestamp()));
}
@Override
protected String getDefaultToAddress() {
return originalSms.getFrom();
return message.getFrom();
}
@Override
@ -29,11 +31,11 @@ public class ForwarderTask extends HttpTask {
app.sendOutgoingMessage(reply);
}
app.setIncomingMessageStatus(originalSms, true);
app.setIncomingMessageStatus(message, true);
}
@Override
protected void handleFailure() {
app.setIncomingMessageStatus(originalSms, false);
app.setIncomingMessageStatus(message, false);
}
}

View File

@ -8,6 +8,8 @@ import android.os.AsyncTask;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -32,7 +34,6 @@ import org.envaya.sms.Base64Coder;
import org.envaya.sms.OutgoingMessage;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
@ -159,6 +160,11 @@ public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
{
post.abort();
app.logError("Error while contacting server", ex);
if (ex instanceof UnknownHostException || ex instanceof SocketTimeoutException)
{
app.asyncCheckConnectivity();
}
return null;
}
catch (Throwable ex)