diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ac1e36c..542e101 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -29,9 +29,6 @@ - - - diff --git a/res/drawable-hdpi/ic_menu_magnet.png b/res/drawable-hdpi/ic_menu_magnet.png new file mode 100755 index 0000000..c1f5cac Binary files /dev/null and b/res/drawable-hdpi/ic_menu_magnet.png differ diff --git a/res/drawable-ldpi/ic_menu_magnet.png b/res/drawable-ldpi/ic_menu_magnet.png new file mode 100755 index 0000000..00d7084 Binary files /dev/null and b/res/drawable-ldpi/ic_menu_magnet.png differ diff --git a/res/drawable-mdpi/ic_menu_magnet.png b/res/drawable-mdpi/ic_menu_magnet.png new file mode 100755 index 0000000..c341d55 Binary files /dev/null and b/res/drawable-mdpi/ic_menu_magnet.png differ diff --git a/res/menu/mainmenu.xml b/res/menu/mainmenu.xml index 1574d9c..4242500 100755 --- a/res/menu/mainmenu.xml +++ b/res/menu/mainmenu.xml @@ -12,4 +12,7 @@ + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index acbed8b..00fd2e7 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5,4 +5,5 @@ Test Connection Check Messages Help + Retry diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index e7b6edb..a1b4c4f 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -4,25 +4,24 @@ */ package org.envaya.kalsms; +import android.app.Activity; import android.app.AlarmManager; import android.app.PendingIntent; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.os.SystemClock; import android.preference.PreferenceManager; import android.telephony.SmsManager; +import android.telephony.SmsMessage; import android.util.Log; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; public class App { @@ -34,21 +33,87 @@ public class App { public static final String STATUS_FAILED = "failed"; public static final String STATUS_SENT = "sent"; - public static final String LOG_NAME = "KALSMS"; + public static final String LOG_NAME = "KALSMS"; public static final String LOG_INTENT = "org.envaya.kalsms.LOG"; - public static final String SEND_STATUS_INTENT = "org.envaya.kalsms.SEND_STATUS"; + + private static App app; + + private Map incomingSmsMap = new HashMap(); + private Map outgoingSmsMap = new HashMap(); public Context context; public SharedPreferences settings; + private class QueuedMessage + { + public T sms; + public long nextAttemptTime = 0; + public int numAttempts = 0; + + public boolean canAttemptNow() + { + return (nextAttemptTime > 0 && nextAttemptTime < System.currentTimeMillis()); + } + + public boolean scheduleNextAttempt() + { + long now = System.currentTimeMillis(); + numAttempts++; + + int sec = 1000; + + if (numAttempts == 1) + { + log("1st failure; retry in 1 minute"); + nextAttemptTime = now + sec * 60; // 1 minute + return true; + } + else if (numAttempts == 2) + { + log("2nd failure; retry in 10 minutes"); + nextAttemptTime = now + sec * 60 * 10; // 10 min + return true; + } + else if (numAttempts == 3) + { + log("3rd failure; retry in 1 hour"); + nextAttemptTime = now + sec * 60 * 60; // 1 hour + return true; + } + else if (numAttempts == 4) + { + log("4th failure: retry in 1 day"); + nextAttemptTime = now + sec * 60 * 60 * 24; // 1 day + return true; + } + else + { + log("5th failure: giving up"); + return false; + } + } + } + + private class QueuedIncomingSms extends QueuedMessage { + public QueuedIncomingSms(SmsMessage sms) + { + this.sms = sms; + } + } + + private class QueuedOutgoingSms extends QueuedMessage { + public QueuedOutgoingSms(OutgoingSmsMessage sms) + { + this.sms = sms; + } + } + protected App(Context context) { this.context = context; this.settings = PreferenceManager.getDefaultSharedPreferences(context); } - private static App app; - public static App getInstance(Context context) { if (app == null) @@ -58,7 +123,7 @@ public class App { return app; } - static void debug(String msg) + public void debug(String msg) { Log.d(LOG_NAME, msg); } @@ -174,60 +239,166 @@ public class App { public String getPassword() { return settings.getString("password", ""); - } - - public SQLiteDatabase getWritableDatabase() + } + + private void notifyStatus(OutgoingSmsMessage sms, String status, String errorMessage) { - return new DBHelper(context).getWritableDatabase(); - } - - public HttpClient getHttpClient() - { - HttpParams httpParameters = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(httpParameters, 8000); - HttpConnectionParams.setSoTimeout(httpParameters, 8000); - return new DefaultHttpClient(httpParameters); - } - - public void sendSMS(OutgoingSmsMessage sms) - { String serverId = sms.getServerId(); - if (serverId != null) + String logMessage; + if (status.equals(App.STATUS_SENT)) { - SQLiteDatabase db = this.getWritableDatabase(); - Cursor cursor = - db.rawQuery("select 1 from sms_status where server_id=?", new String[] { serverId }); + logMessage = "sent successfully"; + } + else if (status.equals(App.STATUS_FAILED)) + { + logMessage = "could not be sent (" + errorMessage + ")"; + } + else + { + logMessage = "queued"; + } + String smsDesc = sms.getLogName(); - boolean exists = (cursor.getCount() > 0); - cursor.close(); - if (exists) - { - log(sms.getLogName() + " already sent, skipping"); - return; - } - - ContentValues values = new ContentValues(); - values.put("server_id", serverId); - values.put("status", App.STATUS_QUEUED); - db.insert("sms_status", null, values); - - db.close(); - } + if (serverId != null) + { + app.log("Notifying server " + smsDesc + " " + logMessage); + new HttpTask(app).execute( + new BasicNameValuePair("id", serverId), + new BasicNameValuePair("status", status), + new BasicNameValuePair("error", errorMessage), + new BasicNameValuePair("action", App.ACTION_SEND_STATUS) + ); + } + else + { + app.log(smsDesc + " " + logMessage); + } + } + + public synchronized void retryStuckMessages(boolean retryAll) + { + retryStuckOutgoingMessages(retryAll); + retryStuckIncomingMessages(retryAll); + } + + public synchronized int getStuckMessageCount() + { + return outgoingSmsMap.size() + incomingSmsMap.size(); + } + + public synchronized void retryStuckOutgoingMessages(boolean retryAll) + { + for (Entry entry : outgoingSmsMap.entrySet()) + { + QueuedOutgoingSms queuedSms = entry.getValue(); + if (retryAll || queuedSms.canAttemptNow()) + { + queuedSms.nextAttemptTime = 0; + + log("Retrying sending " +queuedSms.sms.getLogName() + + " to " + queuedSms.sms.getTo()); + + trySendSMS(queuedSms.sms); + } + } + } + + public synchronized void retryStuckIncomingMessages(boolean retryAll) + { + for (Entry entry : incomingSmsMap.entrySet()) + { + QueuedIncomingSms queuedSms = entry.getValue(); + if (retryAll || queuedSms.canAttemptNow()) + { + queuedSms.nextAttemptTime = 0; + + log("Retrying forwarding SMS from " + queuedSms.sms.getOriginatingAddress()); + + trySendMessageToServer(queuedSms.sms); + } + } + } + + public synchronized void notifyOutgoingMessageStatus(String id, int resultCode) + { + QueuedOutgoingSms queuedSms = outgoingSmsMap.get(id); + + if (queuedSms == null) + { + return; + } + + OutgoingSmsMessage sms = queuedSms.sms; + + switch (resultCode) { + case Activity.RESULT_OK: + this.notifyStatus(sms, App.STATUS_SENT, ""); + break; + case SmsManager.RESULT_ERROR_GENERIC_FAILURE: + this.notifyStatus(sms, App.STATUS_FAILED, "generic failure"); + break; + case SmsManager.RESULT_ERROR_RADIO_OFF: + this.notifyStatus(sms, App.STATUS_FAILED, "radio off"); + break; + case SmsManager.RESULT_ERROR_NO_SERVICE: + this.notifyStatus(sms, App.STATUS_FAILED, "no service"); + break; + case SmsManager.RESULT_ERROR_NULL_PDU: + this.notifyStatus(sms, App.STATUS_FAILED, "null PDU"); + break; + default: + this.notifyStatus(sms, App.STATUS_FAILED, "unknown error"); + break; + } + + switch (resultCode) { + case SmsManager.RESULT_ERROR_GENERIC_FAILURE: + case SmsManager.RESULT_ERROR_RADIO_OFF: + case SmsManager.RESULT_ERROR_NO_SERVICE: + if (!queuedSms.scheduleNextAttempt()) + { + outgoingSmsMap.remove(id); + } + break; + default: + outgoingSmsMap.remove(id); + break; + } + + } + + public synchronized void sendSMS(OutgoingSmsMessage sms) + { + String id = sms.getId(); + if (outgoingSmsMap.containsKey(id)) + { + log(sms.getLogName() + " already sent, skipping"); + return; + } + + QueuedOutgoingSms queueEntry = new QueuedOutgoingSms(sms); + outgoingSmsMap.put(id, queueEntry); + + log("Sending " +sms.getLogName() + " to " + sms.getTo()); + trySendSMS(sms); + } + + private void trySendSMS(OutgoingSmsMessage sms) + { SmsManager smgr = SmsManager.getDefault(); - Intent intent = new Intent(App.SEND_STATUS_INTENT); - intent.putExtra("serverId", serverId); + Intent intent = new Intent(context, MessageStatusNotifier.class); + intent.putExtra("id", sms.getId()); PendingIntent sentIntent = PendingIntent.getBroadcast( this.context, 0, intent, PendingIntent.FLAG_ONE_SHOT); - - log("Sending " +sms.getLogName() + " to " + sms.getTo()); - smgr.sendTextMessage(sms.getTo(), null, sms.getMessage(), sentIntent, null); + + smgr.sendTextMessage(sms.getTo(), null, sms.getMessage(), sentIntent, null); } private class PollerTask extends HttpTask { @@ -245,4 +416,86 @@ public class App { } } + + private class ForwarderTask extends HttpTask { + + private SmsMessage originalSms; + + public ForwarderTask(SmsMessage originalSms) { + super(app); + this.originalSms = originalSms; + } + + @Override + protected String getDefaultToAddress() + { + return originalSms.getOriginatingAddress(); + } + + @Override + protected void handleResponse(HttpResponse response) throws Exception { + + for (OutgoingSmsMessage reply : parseResponseXML(response)) { + app.sendSMS(reply); + } + + app.notifyIncomingMessageStatus(originalSms, true); + } + + @Override + protected void handleFailure() + { + app.notifyIncomingMessageStatus(originalSms, false); + } + } + + private String getSmsId(SmsMessage sms) + { + return sms.getOriginatingAddress() + ":" + sms.getMessageBody() + ":" + sms.getTimestampMillis(); + } + + public synchronized void sendMessageToServer(SmsMessage sms) + { + String id = getSmsId(sms); + if (incomingSmsMap.containsKey(id)) + { + log("Duplicate incoming SMS, skipping"); + return; + } + + QueuedIncomingSms queuedSms = new QueuedIncomingSms(sms); + incomingSmsMap.put(id, queuedSms); + + app.log("Received SMS from " + sms.getOriginatingAddress()); + + trySendMessageToServer(sms); + } + + public void trySendMessageToServer(SmsMessage sms) + { + String message = sms.getMessageBody(); + String sender = sms.getOriginatingAddress(); + + new ForwarderTask(sms).execute( + new BasicNameValuePair("from", sender), + new BasicNameValuePair("message", message), + new BasicNameValuePair("action", App.ACTION_INCOMING) + ); + + } + + private synchronized void notifyIncomingMessageStatus(SmsMessage sms, boolean success) + { + String id = getSmsId(sms); + + QueuedIncomingSms queuedSms = incomingSmsMap.get(id); + + if (queuedSms != null) + { + if (success || !queuedSms.scheduleNextAttempt()) + { + incomingSmsMap.remove(id); + } + } + } } diff --git a/src/org/envaya/kalsms/DBHelper.java b/src/org/envaya/kalsms/DBHelper.java deleted file mode 100755 index 6271823..0000000 --- a/src/org/envaya/kalsms/DBHelper.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * To change this template, choose Tools | Templates - * and open the template in the editor. - */ -package org.envaya.kalsms; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; - -/** - * - * @author Jesse - */ -public class DBHelper extends SQLiteOpenHelper { - - private static final int DATABASE_VERSION = 2; - private static final String DATABASE_NAME = "org.envaya.kalsms.db"; - - private static final String SMS_STATUS_TABLE_DROP = - " DROP TABLE sms_status;"; - - private static final String SMS_STATUS_TABLE_CREATE = - "CREATE TABLE sms_status (server_id text, status int);" - + "CREATE INDEX server_id_index ON sent_sms (server_id);"; - - public DBHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL(SMS_STATUS_TABLE_CREATE); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion < 2) - { - db.execSQL(SMS_STATUS_TABLE_CREATE); - } - } -} diff --git a/src/org/envaya/kalsms/HttpTask.java b/src/org/envaya/kalsms/HttpTask.java index 74fd4db..56c374b 100755 --- a/src/org/envaya/kalsms/HttpTask.java +++ b/src/org/envaya/kalsms/HttpTask.java @@ -19,7 +19,11 @@ import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -36,6 +40,14 @@ public class HttpTask extends AsyncTask this.app = app; } + public HttpClient getHttpClient() + { + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, 8000); + HttpConnectionParams.setSoTimeout(httpParameters, 8000); + return new DefaultHttpClient(httpParameters); + } + private String getSignature(String url, BasicNameValuePair... params) { try { @@ -78,6 +90,12 @@ public class HttpTask extends AsyncTask try { String url = app.getServerUrl(); + + if (url.length() == 0) { + app.log("Can't contact server; Server URL not set"); + return null; + } + HttpPost post = new HttpPost(url); post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params))); @@ -88,7 +106,7 @@ public class HttpTask extends AsyncTask post.setHeader("X-Kalsms-PhoneNumber", app.getPhoneNumber()); post.setHeader("X-Kalsms-Signature", signature); - HttpClient client = app.getHttpClient(); + HttpClient client = getHttpClient(); HttpResponse response = client.execute(post); int statusCode = response.getStatusLine().getStatusCode(); diff --git a/src/org/envaya/kalsms/IncomingMessageForwarder.java b/src/org/envaya/kalsms/IncomingMessageForwarder.java index 27e9120..31fdbe5 100755 --- a/src/org/envaya/kalsms/IncomingMessageForwarder.java +++ b/src/org/envaya/kalsms/IncomingMessageForwarder.java @@ -5,79 +5,13 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.telephony.SmsMessage; -import java.util.ArrayList; -import java.util.List; import org.apache.http.HttpResponse; import org.apache.http.message.BasicNameValuePair; public class IncomingMessageForwarder extends BroadcastReceiver { - private App app; - - private List retryList = new ArrayList(); - - private class SmsStatus - { - public SmsMessage smsMessage; - public long nextAttemptTime; - public int numAttempts = 0; - - } - - private class ForwarderTask extends HttpTask { - - private SmsMessage originalSms; - - public ForwarderTask(SmsMessage originalSms) { - super(app); - this.originalSms = originalSms; - } - - @Override - protected String getDefaultToAddress() - { - return originalSms.getOriginatingAddress(); - } - - @Override - protected void handleResponse(HttpResponse response) throws Exception { - for (OutgoingSmsMessage reply : parseResponseXML(response)) { - app.sendSMS(reply); - } - } - } - - public void sendMessageToServer(SmsMessage sms) - { - String serverUrl = app.getServerUrl(); - String message = sms.getMessageBody(); - String sender = sms.getOriginatingAddress(); - - app.log("Received SMS from " + sender); - - if (serverUrl.length() == 0) { - app.log("Can't forward SMS to server; Server URL not set"); - } else { - app.log("Forwarding incoming SMS to server"); - - new ForwarderTask(sms).execute( - new BasicNameValuePair("from", sender), - new BasicNameValuePair("message", message), - new BasicNameValuePair("action", App.ACTION_INCOMING) - ); - } - } - - public void smsReceived(Intent intent) { - - for (SmsMessage sms : getMessagesFromIntent(intent)) { - sendMessageToServer(sms); - - //DeleteSMSFromInbox(context, mesg); - } - - } + private App app; @Override // source: http://www.devx.com/wireless/Article/39495/1954 @@ -88,7 +22,12 @@ public class IncomingMessageForwarder extends BroadcastReceiver { String action = intent.getAction(); if (action.equals("android.provider.Telephony.SMS_RECEIVED")) { - smsReceived(intent); + + for (SmsMessage sms : getMessagesFromIntent(intent)) { + app.sendMessageToServer(sms); + + //DeleteSMSFromInbox(context, mesg); + } } } catch (Throwable ex) { app.logError("Unexpected error in IncomingMessageForwarder", ex, true); diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java index cfa9a5c..636bfc2 100755 --- a/src/org/envaya/kalsms/Main.java +++ b/src/org/envaya/kalsms/Main.java @@ -92,22 +92,14 @@ public class Main extends Activity { } }); } } - - - public void onResume() { - App.debug("RESUME"); - super.onResume(); - } - + /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - App.debug("STARTED"); - this.app = App.getInstance(this.getApplication()); - + setContentView(R.layout.main); PreferenceManager.setDefaultValues(this, R.xml.prefs, false); @@ -138,6 +130,9 @@ public class Main extends Activity { case R.id.check_now: app.checkOutgoingMessages(); return true; + case R.id.retry_now: + app.retryStuckMessages(true); + return true; case R.id.help: startActivity(new Intent(this, Help.class)); return true; @@ -161,6 +156,14 @@ public class Main extends Activity { return(true); } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem item = menu.findItem(R.id.retry_now); + int stuckMessages = app.getStuckMessageCount(); + item.setEnabled(stuckMessages > 0); + item.setTitle("Retry Now (" + stuckMessages + ")"); + return true; + } @Override protected void onStop(){ diff --git a/src/org/envaya/kalsms/MessageStatusNotifier.java b/src/org/envaya/kalsms/MessageStatusNotifier.java index b4b6da0..669fc28 100755 --- a/src/org/envaya/kalsms/MessageStatusNotifier.java +++ b/src/org/envaya/kalsms/MessageStatusNotifier.java @@ -4,76 +4,21 @@ */ package org.envaya.kalsms; -import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.telephony.SmsManager; -import org.apache.http.message.BasicNameValuePair; public class MessageStatusNotifier extends BroadcastReceiver { - private App app; - - public void notifyStatus(String serverId, String status, String errorMessage) - { - String logMessage; - if (status.equals(App.STATUS_SENT)) - { - logMessage = "sent successfully"; - } - else if (status.equals(App.STATUS_FAILED)) - { - logMessage = "could not be sent (" + errorMessage + ")"; - } - else - { - logMessage = "queued"; - } - String smsDesc = serverId == null ? "SMS reply" : ("SMS id=" + serverId); - - if (serverId != null) - { - app.log("Notifying server " + smsDesc + " " + logMessage); - - new HttpTask(app).execute( - new BasicNameValuePair("id", serverId), - new BasicNameValuePair("status", status), - new BasicNameValuePair("error", errorMessage), - new BasicNameValuePair("action", App.ACTION_SEND_STATUS) - ); - } - else - { - app.log(smsDesc + " " + logMessage); - } - } - @Override public void onReceive(Context context, Intent intent) { - app = App.getInstance(context); - - String serverId = intent.getExtras().getString("serverId"); + App app = App.getInstance(context); - switch (getResultCode()) { - case Activity.RESULT_OK: - this.notifyStatus(serverId, App.STATUS_SENT, ""); - break; - case SmsManager.RESULT_ERROR_GENERIC_FAILURE: - this.notifyStatus(serverId, App.STATUS_FAILED, "generic failure"); - break; - case SmsManager.RESULT_ERROR_RADIO_OFF: - this.notifyStatus(serverId, App.STATUS_FAILED, "radio off"); - break; - case SmsManager.RESULT_ERROR_NO_SERVICE: - this.notifyStatus(serverId, App.STATUS_FAILED, "no service"); - break; - case SmsManager.RESULT_ERROR_NULL_PDU: - this.notifyStatus(serverId, App.STATUS_FAILED, "null PDU"); - break; - default: - this.notifyStatus(serverId, App.STATUS_FAILED, "unknown error"); - break; - } + String id = intent.getExtras().getString("id"); + + int resultCode = getResultCode(); + + app.notifyOutgoingMessageStatus(id, resultCode); } } diff --git a/src/org/envaya/kalsms/OutgoingMessagePoller.java b/src/org/envaya/kalsms/OutgoingMessagePoller.java index a8c9018..6d56aff 100755 --- a/src/org/envaya/kalsms/OutgoingMessagePoller.java +++ b/src/org/envaya/kalsms/OutgoingMessagePoller.java @@ -12,5 +12,6 @@ public class OutgoingMessagePoller extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { app = App.getInstance(context); app.checkOutgoingMessages(); + app.retryStuckMessages(false); } } diff --git a/src/org/envaya/kalsms/OutgoingSmsMessage.java b/src/org/envaya/kalsms/OutgoingSmsMessage.java index bed824a..8b7ddf5 100755 --- a/src/org/envaya/kalsms/OutgoingSmsMessage.java +++ b/src/org/envaya/kalsms/OutgoingSmsMessage.java @@ -6,10 +6,25 @@ public class OutgoingSmsMessage { private String serverId; private String message; private String from; - private String to; + private String to; + + private String localId; + + private static int nextLocalId = 1; public OutgoingSmsMessage() - { + { + this.localId = "_o" + getNextLocalId(); + } + + static synchronized int getNextLocalId() + { + return nextLocalId++; + } + + public String getId() + { + return (serverId == null) ? localId : serverId; } public String getLogName()