From 1081f5758025b09d5f3d26816f162ca6f846d25a Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Thu, 29 Sep 2011 16:02:37 -0700 Subject: [PATCH] automatic failover between wifi/mobile if server cannot be reached; send timestamp of incoming message to server --- AndroidManifest.xml | 21 +- res/xml/prefs.xml | 7 + server/php/EnvayaSMS.php | 2 + src/org/envaya/sms/App.java | 213 +++++++++++++++++- src/org/envaya/sms/IncomingMessage.java | 12 +- src/org/envaya/sms/IncomingMms.java | 7 +- src/org/envaya/sms/IncomingSms.java | 14 +- src/org/envaya/sms/MmsUtils.java | 9 +- src/org/envaya/sms/QueuedMessage.java | 8 +- .../receiver/ConnectivityChangeReceiver.java | 24 ++ .../DequeueOutgoingMessageReceiver.java | 6 + .../sms/receiver/IncomingMessageRetry.java | 5 + .../sms/receiver/OutgoingMessageRetry.java | 6 +- .../sms/receiver/ReenableWifiReceiver.java | 29 +++ src/org/envaya/sms/task/ForwarderTask.java | 18 +- src/org/envaya/sms/task/HttpTask.java | 8 +- 16 files changed, 350 insertions(+), 39 deletions(-) create mode 100755 src/org/envaya/sms/receiver/ConnectivityChangeReceiver.java create mode 100755 src/org/envaya/sms/receiver/ReenableWifiReceiver.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 9262cdf..ed76597 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,11 +1,15 @@ + android:versionCode="12" + android:versionName="2.0-rc2"> + + + + @@ -78,7 +82,10 @@ - + + + + @@ -95,11 +102,17 @@ + + + + + + - + \ No newline at end of file diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index ed60e72..6d88b20 100755 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -54,6 +54,13 @@ > + + 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) { diff --git a/src/org/envaya/sms/App.java b/src/org/envaya/sms/App.java index 0a8a7b5..0a422af 100755 --- a/src/org/envaya/sms/App.java +++ b/src/org/envaya/sms/App.java @@ -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 incomingMessages = new HashMap(); private Map outgoingMessages = new HashMap(); @@ -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 connectivityCheckStates + = new HashMap(); + + 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... + } } diff --git a/src/org/envaya/sms/IncomingMessage.java b/src/org/envaya/sms/IncomingMessage.java index 14ce323..5feee08 100755 --- a/src/org/envaya/sms/IncomingMessage.java +++ b/src/org/envaya/sms/IncomingMessage.java @@ -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() diff --git a/src/org/envaya/sms/IncomingMms.java b/src/org/envaya/sms/IncomingMms.java index 106ccd2..f3f5f2c 100755 --- a/src/org/envaya/sms/IncomingMms.java +++ b/src/org/envaya/sms/IncomingMms.java @@ -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(); 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()) diff --git a/src/org/envaya/sms/IncomingSms.java b/src/org/envaya/sms/IncomingSms.java index a19a039..279b7b9 100755 --- a/src/org/envaya/sms/IncomingSms.java +++ b/src/org/envaya/sms/IncomingSms.java @@ -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 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(); diff --git a/src/org/envaya/sms/MmsUtils.java b/src/org/envaya/sms/MmsUtils.java index 905efac..4025471 100755 --- a/src/org/envaya/sms/MmsUtils.java +++ b/src/org/envaya/sms/MmsUtils.java @@ -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 messages = new ArrayList(); @@ -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)); diff --git a/src/org/envaya/sms/QueuedMessage.java b/src/org/envaya/sms/QueuedMessage.java index d19bfb5..764911b 100755 --- a/src/org/envaya/sms/QueuedMessage.java +++ b/src/org/envaya/sms/QueuedMessage.java @@ -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; diff --git a/src/org/envaya/sms/receiver/ConnectivityChangeReceiver.java b/src/org/envaya/sms/receiver/ConnectivityChangeReceiver.java new file mode 100755 index 0000000..6683b5e --- /dev/null +++ b/src/org/envaya/sms/receiver/ConnectivityChangeReceiver.java @@ -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(); + } +} diff --git a/src/org/envaya/sms/receiver/DequeueOutgoingMessageReceiver.java b/src/org/envaya/sms/receiver/DequeueOutgoingMessageReceiver.java index ef0063a..9265710 100755 --- a/src/org/envaya/sms/receiver/DequeueOutgoingMessageReceiver.java +++ b/src/org/envaya/sms/receiver/DequeueOutgoingMessageReceiver.java @@ -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(); } } diff --git a/src/org/envaya/sms/receiver/IncomingMessageRetry.java b/src/org/envaya/sms/receiver/IncomingMessageRetry.java index 18e2a62..5b68a9f 100755 --- a/src/org/envaya/sms/receiver/IncomingMessageRetry.java +++ b/src/org/envaya/sms/receiver/IncomingMessageRetry.java @@ -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()); } } diff --git a/src/org/envaya/sms/receiver/OutgoingMessageRetry.java b/src/org/envaya/sms/receiver/OutgoingMessageRetry.java index 6d17a0e..1c477e2 100755 --- a/src/org/envaya/sms/receiver/OutgoingMessageRetry.java +++ b/src/org/envaya/sms/receiver/OutgoingMessageRetry.java @@ -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()); } } diff --git a/src/org/envaya/sms/receiver/ReenableWifiReceiver.java b/src/org/envaya/sms/receiver/ReenableWifiReceiver.java new file mode 100755 index 0000000..779f720 --- /dev/null +++ b/src/org/envaya/sms/receiver/ReenableWifiReceiver.java @@ -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); + } + } +} diff --git a/src/org/envaya/sms/task/ForwarderTask.java b/src/org/envaya/sms/task/ForwarderTask.java index a01f295..b4f8089 100755 --- a/src/org/envaya/sms/task/ForwarderTask.java +++ b/src/org/envaya/sms/task/ForwarderTask.java @@ -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); } } diff --git a/src/org/envaya/sms/task/HttpTask.java b/src/org/envaya/sms/task/HttpTask.java index 38cc811..74e89e1 100755 --- a/src/org/envaya/sms/task/HttpTask.java +++ b/src/org/envaya/sms/task/HttpTask.java @@ -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 { { post.abort(); app.logError("Error while contacting server", ex); + + if (ex instanceof UnknownHostException || ex instanceof SocketTimeoutException) + { + app.asyncCheckConnectivity(); + } return null; } catch (Throwable ex)