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)