From 4130593f3d0ee1f9c6ce7744ce905a7a490d49e6 Mon Sep 17 00:00:00 2001 From: Nir Yariv Date: Sat, 15 Jan 2011 12:26:06 -0500 Subject: [PATCH 01/69] polling --- README.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.txt b/README.txt index 4695d8f..3c2335b 100644 --- a/README.txt +++ b/README.txt @@ -1,3 +1,5 @@ +(Polling branch) + KalSMS is an simple Android SMS gateway application. Info & setup here: http://wiki.github.com/niryariv/kalsms/ From aee1061c5a89732eca1cfecfd72f3b5eb3cd048b Mon Sep 17 00:00:00 2001 From: Nir Yariv Date: Sat, 15 Jan 2011 14:42:26 -0500 Subject: [PATCH 02/69] initial import of polling code --- AndroidManifest.xml | 10 +- README.txt | 2 +- res/xml/prefs.xml | 6 +- src/kalsms/niryariv/itp/Main.java | 2 - src/kalsms/niryariv/itp/Prefs.java | 55 +++++++- src/kalsms/niryariv/itp/SMSReceiver.java | 124 +----------------- src/kalsms/niryariv/itp/SMSSender.java | 53 ++++++++ src/kalsms/niryariv/itp/TargetUrlRequest.java | 105 +++++++++++++++ src/kalsms/niryariv/itp/URLopen.java | 74 ----------- 9 files changed, 226 insertions(+), 205 deletions(-) mode change 100644 => 100755 src/kalsms/niryariv/itp/Prefs.java create mode 100755 src/kalsms/niryariv/itp/SMSSender.java create mode 100644 src/kalsms/niryariv/itp/TargetUrlRequest.java delete mode 100644 src/kalsms/niryariv/itp/URLopen.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 0af7a82..7841597 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4,10 +4,11 @@ android:versionCode="1" android:versionName="1.0"> - + + @@ -17,12 +18,15 @@ - - + + + + + diff --git a/README.txt b/README.txt index 3c2335b..e07f1cb 100644 --- a/README.txt +++ b/README.txt @@ -1,4 +1,4 @@ -(Polling branch) +(Polling branch - this adds periodic URL polling functionality) KalSMS is an simple Android SMS gateway application. diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index 2692eac..449faf9 100644 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -2,6 +2,8 @@ - - + + + + \ No newline at end of file diff --git a/src/kalsms/niryariv/itp/Main.java b/src/kalsms/niryariv/itp/Main.java index 3f5328f..1c9f5d3 100644 --- a/src/kalsms/niryariv/itp/Main.java +++ b/src/kalsms/niryariv/itp/Main.java @@ -4,8 +4,6 @@ import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; -import android.preference.PreferenceActivity; -import android.preference.Preference; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Html; diff --git a/src/kalsms/niryariv/itp/Prefs.java b/src/kalsms/niryariv/itp/Prefs.java old mode 100644 new mode 100755 index 10b3fa1..ab02076 --- a/src/kalsms/niryariv/itp/Prefs.java +++ b/src/kalsms/niryariv/itp/Prefs.java @@ -1,31 +1,80 @@ package kalsms.niryariv.itp; import kalsms.niryariv.itp.R; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Bundle; +import android.os.SystemClock; +import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceActivity; +import android.preference.PreferenceManager; import android.util.Log; +import android.view.Menu; -public class Prefs extends PreferenceActivity { +public class Prefs extends PreferenceActivity implements OnSharedPreferenceChangeListener { + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.prefs); } + protected void onResume() { + super.onResume(); + // Set up a listener whenever a key changes + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + protected void onPause() { + super.onPause(); + // Unregister the listener whenever a key changes + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { Preference pref = findPreference(key); - if (pref instanceof EditTextPreference) { EditTextPreference textPref = (EditTextPreference) pref; pref.setSummary(textPref.getSummary()); Log.d("KALSMS", "textPref.getSummary(): " + textPref.getSummary()); } + if(pref instanceof CheckBoxPreference) { + CheckBoxPreference checkbox = (CheckBoxPreference) pref; + AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent pintent = new Intent(this, SMSSender.class); + PendingIntent pIntent = PendingIntent.getBroadcast(this,0,pintent, 0); + if(checkbox.isChecked()) { + long interval = 60*Integer.valueOf(PreferenceManager.getDefaultSharedPreferences(this).getString("pref_poll_interval", "5000"));//5mins;//5mins + long firstPoll = SystemClock.elapsedRealtime() + 60*Integer.valueOf(PreferenceManager.getDefaultSharedPreferences(this).getString("pref_poll_interval", "5000")); + alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, firstPoll, interval, pIntent); + Log.d("KALSMS", "alarm manager turned on "+interval); + }else { + alarm.cancel(pIntent); + Log.d("SMS_GATEWAY", "alarm manager turned off"); + } + } + } + + // first time the Menu key is pressed + public boolean onCreateOptionsMenu(Menu menu) { + startActivity(new Intent(this, Prefs.class)); + return(true); + } + + // any other time the Menu key is pressed + public boolean onPrepareOptionsMenu(Menu menu) { + startActivity(new Intent(this, Prefs.class)); + return(true); } } diff --git a/src/kalsms/niryariv/itp/SMSReceiver.java b/src/kalsms/niryariv/itp/SMSReceiver.java index 207774c..5394982 100644 --- a/src/kalsms/niryariv/itp/SMSReceiver.java +++ b/src/kalsms/niryariv/itp/SMSReceiver.java @@ -1,15 +1,6 @@ package kalsms.niryariv.itp; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; import java.util.ArrayList; -import java.util.List; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -22,27 +13,7 @@ import android.telephony.SmsMessage; import android.util.Log; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.NameValuePair; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.util.EntityUtils; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - public class SMSReceiver extends BroadcastReceiver { - @Override // source: http://www.devx.com/wireless/Article/39495/1954 @@ -53,6 +24,7 @@ public void onReceive(Context context, Intent intent) { // get settings SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); + TargetUrlRequest url = new TargetUrlRequest(); String identifier = settings.getString("pref_identifier", ""); String targetUrl = settings.getString("pref_target_url", ""); @@ -66,25 +38,21 @@ public void onReceive(Context context, Intent intent) { if (message != null && message.length() > 0 && (message.toLowerCase().startsWith(identifier) || identifier.trim() == "")) { - Log.d("KALSMS", "MSG RCVD:\"" + message + "\" from: " + sender); // send the message to the URL - String resp = openURL(sender, message, targetUrl).toString(); - + String resp = url.openURL(sender, message, targetUrl).toString(); Log.d("KALSMS", "RESP:\"" + resp); // SMS back the response if (resp.trim().length() > 0) { - ArrayList> items = parseXML(resp); + ArrayList> items = url.parseXML(resp); SmsManager smgr = SmsManager.getDefault(); - for (int j = 0; j < items.size(); j++) { String sendTo = items.get(j).get(0); if (sendTo.toLowerCase() == "sender") sendTo = sender; String sendMsg = items.get(j).get(1); - try { Log.d("KALSMS", "SEND MSG:\"" + sendMsg + "\" TO: " + sendTo); smgr.sendTextMessage(sendTo, null, sendMsg, null, null); @@ -93,21 +61,16 @@ public void onReceive(Context context, Intent intent) { } } } - // delete SMS from inbox, to prevent it from filling up DeleteSMSFromInbox(context, mesg); - } } - } private void DeleteSMSFromInbox(Context context, SmsMessage mesg) { Log.d("KALSMS", "try to delete SMS"); - try { Uri uriSms = Uri.parse("content://sms/inbox"); - StringBuilder sb = new StringBuilder(); sb.append("address='" + mesg.getOriginatingAddress() + "' AND "); sb.append("body='" + mesg.getMessageBody() + "'"); @@ -138,88 +101,9 @@ private SmsMessage[] getMessagesFromIntent(Intent intent) { byte[] byteData = (byte[]) pdus[n]; retMsgs[n] = SmsMessage.createFromPdu(byteData); } - } catch (Exception e) { Log.e("KALSMS", "GetMessages ERROR\n" + e); } return retMsgs; } - - - public String openURL(String sender, String message, String targetUrl) { - - List qparams = new ArrayList(); - qparams.add(new BasicNameValuePair("sender", sender)); - qparams.add(new BasicNameValuePair("msg", message)); - String url = targetUrl + "?" + URLEncodedUtils.format(qparams, "UTF-8"); - - try { - HttpClient client = new DefaultHttpClient(); - HttpGet get = new HttpGet(url); - - HttpResponse responseGet = client.execute(get); - HttpEntity resEntityGet = responseGet.getEntity(); - if (resEntityGet != null) { - String resp = EntityUtils.toString(resEntityGet); - Log.e("KALSMS", "HTTP RESP" + resp); - return resp; - } - } catch (Exception e) { - Log.e("KALSMS", "HTTP REQ FAILED:" + url); - e.printStackTrace(); - } - - return ""; - } - - - public static ArrayList> parseXML(String xml) { - ArrayList> output = new ArrayList>(); - - try { - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - - Document doc = dBuilder.parse(new InputSource(new StringReader(xml))); - - NodeList rnodes = doc.getElementsByTagName("reply"); - - NodeList nodes = rnodes.item(0).getChildNodes(); - - for (int i=0; i < nodes.getLength(); i++) { - try { - List item = new ArrayList(); - - Node node = nodes.item(i); - if (node.getNodeType() != Node.ELEMENT_NODE) continue; - - Element e = (Element) node; - String nodeName = e.getNodeName(); - - if (nodeName.equalsIgnoreCase("sms")) { - if (!e.getAttribute("phone").equals("")) { - item.add(e.getAttribute("phone")); - item.add(e.getFirstChild().getNodeValue()); - output.add((ArrayList) item); - } - } else if (nodeName.equalsIgnoreCase("sms-to-sender")) { - item.add("sender"); - item.add(e.getFirstChild().getNodeValue()); - output.add((ArrayList) item); - } else { - continue; - } - } catch (Exception e){ - Log.e("KALSMS", "FAILED PARSING XML NODE# " + i ); - } - } - Log.e("KALSMS", "PARSING XML RETURNS " + output ); - return (output); - - } catch (Exception e) { - Log.e("KALSMS", "PARSING XML FAILED: " + xml ); - e.printStackTrace(); - return (output); - } - } -} +} \ No newline at end of file diff --git a/src/kalsms/niryariv/itp/SMSSender.java b/src/kalsms/niryariv/itp/SMSSender.java new file mode 100755 index 0000000..004c629 --- /dev/null +++ b/src/kalsms/niryariv/itp/SMSSender.java @@ -0,0 +1,53 @@ +package kalsms.niryariv.itp; + +import java.util.ArrayList; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.PowerManager; +import android.preference.PreferenceManager; +import android.telephony.SmsManager; +import android.util.Log; + +public class SMSSender extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + + // acquiring the wake clock to prevent device from sleeping while request is processed + final PowerManager pm = (PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wake = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "http_request"); + wake.acquire(); + + // get settings + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); + String targetUrl = settings.getString("pref_target_url", ""); + Log.d("KALSMS", "url:\"" + targetUrl); + TargetUrlRequest url = new TargetUrlRequest(); + // send the message to the URL + String resp = url.openURL("","",targetUrl).toString(); + + Log.d("KALSMS", "RESP:\"" + resp); + + // SMS back the response + if (resp.trim().length() > 0) { + ArrayList> items = url.parseXML(resp); + + SmsManager smgr = SmsManager.getDefault(); + + for (int j = 0; j < items.size(); j++) { + String sendTo = items.get(j).get(0); + String sendMsg = items.get(j).get(1); + + try { + Log.d("KALSMS", "SEND MSG:\"" + sendMsg + "\" TO: " + sendTo); + smgr.sendTextMessage(sendTo, null, sendMsg, null, null); + } catch (Exception ex) { + Log.d("KALSMS", "SMS FAILED"); + } + } + } + wake.release(); + } +} diff --git a/src/kalsms/niryariv/itp/TargetUrlRequest.java b/src/kalsms/niryariv/itp/TargetUrlRequest.java new file mode 100644 index 0000000..ede5d55 --- /dev/null +++ b/src/kalsms/niryariv/itp/TargetUrlRequest.java @@ -0,0 +1,105 @@ +package kalsms.niryariv.itp; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import android.util.Log; + +public class TargetUrlRequest { + + public String openURL(String sender, String message, String targetUrl) { + + String url = targetUrl; + if(sender.trim().length() > 0 && message.trim().length() > 0) { + List qparams = new ArrayList(); + qparams.add(new BasicNameValuePair("sender", sender)); + qparams.add(new BasicNameValuePair("msg", message)); + url = targetUrl + "?" + URLEncodedUtils.format(qparams, "UTF-8"); + } + try { + HttpClient client = new DefaultHttpClient(); + HttpGet get = new HttpGet(url); + + HttpResponse responseGet = client.execute(get); + HttpEntity resEntityGet = responseGet.getEntity(); + if (resEntityGet != null) { + String resp = EntityUtils.toString(resEntityGet); + Log.e("KALSMS", "HTTP RESP" + resp); + return resp; + } + } catch (Exception e) { + Log.e("KALSMS", "HTTP REQ FAILED:" + url); + e.printStackTrace(); + } + return ""; + } + + public ArrayList> parseXML(String xml) { + ArrayList> output = new ArrayList>(); + + try { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + + Document doc = dBuilder.parse(new InputSource(new StringReader(xml))); + + NodeList rnodes = doc.getElementsByTagName("reply"); + + NodeList nodes = rnodes.item(0).getChildNodes(); + + for (int i=0; i < nodes.getLength(); i++) { + try { + List item = new ArrayList(); + + Node node = nodes.item(i); + if (node.getNodeType() != Node.ELEMENT_NODE) continue; + + Element e = (Element) node; + String nodeName = e.getNodeName(); + + if (nodeName.equalsIgnoreCase("sms")) { + if (!e.getAttribute("phone").equals("")) { + item.add(e.getAttribute("phone")); + item.add(e.getFirstChild().getNodeValue()); + output.add((ArrayList) item); + } + } else if (nodeName.equalsIgnoreCase("sms-to-sender")) { + item.add("sender"); + item.add(e.getFirstChild().getNodeValue()); + output.add((ArrayList) item); + } else { + continue; + } + } catch (Exception e){ + Log.e("KALSMS", "FAILED PARSING XML NODE# " + i ); + } + } + Log.e("KALSMS", "PARSING XML RETURNS " + output ); + return (output); + + } catch (Exception e) { + Log.e("KALSMS", "PARSING XML FAILED: " + xml ); + e.printStackTrace(); + return (output); + } + } +} diff --git a/src/kalsms/niryariv/itp/URLopen.java b/src/kalsms/niryariv/itp/URLopen.java deleted file mode 100644 index 4aa8c1f..0000000 --- a/src/kalsms/niryariv/itp/URLopen.java +++ /dev/null @@ -1,74 +0,0 @@ -package kalsms.niryariv.itp; -//package txtgate.niryariv.itp; -// -//import java.io.BufferedInputStream; -//import java.io.InputStream; -//import java.net.URL; -//import java.net.URLConnection; -// -//import org.apache.http.util.ByteArrayBuffer; -// -//public class URLopen { -// private Thread checkUpdate = new Thread() { -// public void run() { -// try { -// URL updateURL = new URL("http://iconic.4feets.com/update"); -// URLConnection conn = updateURL.openConnection(); -// InputStream is = conn.getInputStream(); -// BufferedInputStream bis = new BufferedInputStream(is); -// ByteArrayBuffer baf = new ByteArrayBuffer(50); -// -// int current = 0; -// while((current = bis.read()) != -1){ -// baf.append((byte)current); -// } -// -// /* Convert the Bytes read to a String. */ -// final String s = new String(baf.toByteArray()); -//// mHandler.post(showUpdate); -// } catch (Exception e) { -// // -// } -// } -// }; -//} -// -////public class Iconic extends Activity { -//// private String html = ""; -//// private Handler mHandler; -//// -//// public void onCreate(Bundle savedInstanceState) { -//// super.onCreate(savedInstanceState); -//// setContentView(R.layout.main); -//// mHandler = new Handler(); -//// checkUpdate.start(); -//// } -//// -//// private Thread checkUpdate = new Thread() { -//// public void run() { -//// try { -//// URL updateURL = new URL("http://iconic.4feets.com/update"); -//// URLConnection conn = updateURL.openConnection(); -//// InputStream is = conn.getInputStream(); -//// BufferedInputStream bis = new BufferedInputStream(is); -//// ByteArrayBuffer baf = new ByteArrayBuffer(50); -//// -//// int current = 0; -//// while((current = bis.read()) != -1){ -//// baf.append((byte)current); -//// } -//// -//// /* Convert the Bytes read to a String. */ -//// html = new String(baf.toByteArray()); -//// mHandler.post(showUpdate); -//// } catch (Exception e) { -//// } -//// } -//// }; -//// -//// private Runnable showUpdate = new Runnable(){ -//// public void run(){ -//// Toast.makeText(Iconic.this, "HTML Code: " + html, Toast.LENGTH_SHORT).show(); -//// } -//// }; -////} \ No newline at end of file From 8e31033862123031f8a81597427f314cb9f3fc8f Mon Sep 17 00:00:00 2001 From: Nir Yariv Date: Sat, 15 Jan 2011 17:44:06 -0500 Subject: [PATCH 03/69] new icon --- res/drawable-hdpi/icon.png | Bin 4147 -> 11293 bytes res/drawable-ldpi/icon.png | Bin 1723 -> 5051 bytes res/drawable-mdpi/icon.png | Bin 2574 -> 6754 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png index 8074c4c571b8cd19e27f4ee5545df367420686d7..c091fd76a44a7379f538f1931e755c518734a3c0 100644 GIT binary patch literal 11293 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 diff --git a/res/drawable-ldpi/icon.png b/res/drawable-ldpi/icon.png index 1095584ec21f71cd0afc9e0993aa2209671b590c..dde022d645fcc87804f100a75bc7b461d69f3ff1 100644 GIT binary patch literal 5051 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 Ef-18SF8}}l delta 2565 zcmV+g3i|cpG>#OIBYz3iNklQ`Og{P|9^0MHK0_ue5La$BvqW(E{R@m;+b z+04~`K@~=KAlzM05bd(Nnm)?dxa?uEJXv zeu+q80Dqrd@f`f#Ip`ZXfIAz$i21em;CTN_=xEu3VAVXlcIsQuGdfb46xxDaSk?R` zH2PP;NRz%=?4~pTga_dz#}k`EC}jbv!cw~BHsU&lZlE=|p3FXv_J;eB)>By1vJIEU zj-b8%el*o|VkG)LqNx#TG>Jvj^jIte!!+RY)PJLY@)%lY-vfVDGlHHLY+U$V^iLdv zrg-Q+#?(5pRz7B(*J9e@sgV7wrlqlyV{Kn>8(4v2;-jwVy`N*NVyb04`0ujrO_sVN?6}QB&QD z4GSJe%j~t7rPZQi-XruqkF`oeU>Uj=euKg#v9@g+*0gTHJN>UCC)o`?7zD@J3ChBdeQ!oN8-bK z)HDs<-97KETX)x2xXJ)=m{^cXOT|Ert2__P^~`+Z&V#E9S;5N`C++P3qk^?v1rVm~ zjBq;ys6=tD0*LR676fk>l#4%C0c@r#(S$vBrrWXt+)4|uIHPyU|>La6h%92y4IPufI$9>Xu!@y z`TaNgtg&41@PwMwBdmSm7)xAWDStg10dJ$le#VeeKMN{LP+@#9)=Dy}s; zItmd)fR<7PtqBfRa1}tbFQyiv?qISgtA=i5gaug^9(5La2LFf?gTH4Ln}4k+ErF;= z-IQ`Bh0yq$6zokLP=AUI^S+6dO%L-+9m!-89*>7Y@(fexVMT0Y#VqQ+u?`Qld2!+h zqUjMX>VbzxMA73u+GgKvc?r?JS(VpwP!0HUiC7TuULkc_CJBKkqFd8gJbY!X=N|2>}SVTgb;KG z8CTHJxE0O8PV71L7+pwiVw}GVjqS6>G`g99Zl)=$51*L-G}qEGnSbbqHaCQ79+E=u zUXCOZ35jAMRz%R%0(P!0FMv=sk>Nr8%+OzY^c-M9@+ zfz=G`qj2+%Zca?2GF_~Ag8Z{+AH9ziZejL@FI=nNq3nz#ELXAuA;T*>;hBqKa81dvd z<6{SYg-4wiWq)s33zYnF+JI;-;N=Ylg%+L3uVj~+sx~~d^k>|VJ=ov3eJauU2EUa}8?jjAVjIdWLN~x|EkQziRAeJZ(CKl9w41uafUTzJe$=`uZi++SY2K7JA ze8&>W!#h=xt|D?2FrFI2zR-_dmv$9EzO;Scl}c_fpno-CaqR=VUaUvk>@F_89b@tH zs8R!*QKY;INJ<2_U+K6Ca3e9Gsl2{qY0%a7J?uICWgHuLfj+MB=GkAN1&ifT#2u}B z+2S#~$5jA(Qn^;H%CCmIae4AE-Dsng|Hl*Ov!z72k3ZnJs{pp|+pW`DDueC#mEWOf z=ucJ!dVgwUH@>j^1>TL0u`W8w;Jx`gA+b`%0Pb1H4q!VDoLX66B>p}bl~W&?H8pNf zf56)2Czv7W0=b~Qsa8^~W179L%2Mj#vRQCqI=|0g@wJLYT$WSW0u+`LdvR`TFYo?W zayL$LjI=%o=2*#5$-d=&T?LTKRr`!ux`mZK+kfjHWDJu_CL5173+G1viO%M)LDgs( zNkBJ&R^GCHEN-WinlC}A7?9kRIDY*2SZ{CdB=-Xmlu3c=bp$!2FUGt5ujP&Eh%~%T zAMQdgEh5EUr9e)lIFdhgjYQNO<97+7V?K4FDt+}iT+enfv5qJ;aLMZpj@?R-eY*9u||RB?bS+IKo}Kc5yWnb blIs5eNSY+j744LC00000NkvXXu0mjf=abzo From ec1dc62631a7bebb42f221b2b1f93898b8837e34 Mon Sep 17 00:00:00 2001 From: Nir Yariv Date: Sat, 15 Jan 2011 18:38:49 -0500 Subject: [PATCH 04/69] simplify polling UI to use fixed, 15 min intervals --- res/xml/prefs.xml | 25 +++++++++++++++++++++---- src/kalsms/niryariv/itp/Main.java | 14 +++++++++++--- src/kalsms/niryariv/itp/Prefs.java | 9 +++------ 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index 449faf9..2022e3a 100644 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -2,8 +2,25 @@ - - - - + + + + + + \ No newline at end of file diff --git a/src/kalsms/niryariv/itp/Main.java b/src/kalsms/niryariv/itp/Main.java index 1c9f5d3..1409a95 100644 --- a/src/kalsms/niryariv/itp/Main.java +++ b/src/kalsms/niryariv/itp/Main.java @@ -17,7 +17,7 @@ public class Main extends Activity { public String identifier = ""; public String targetUrl = ""; - + public Boolean polling = false; public void onResume() { Log.d("KALSMS", "RESUME"); @@ -27,11 +27,14 @@ public void onResume() { this.identifier = settings.getString("pref_identifier", ""); this.targetUrl = settings.getString("pref_target_url", ""); - + this.polling = settings.getBoolean("pref_poll_switch", false); + Log.d("KALSMS", "onResume ident:" + this.identifier +"\ntarget:" + this.targetUrl); String infoText = new String(); + // Home Screen text + infoText = "All SMS messages"; if (this.identifier.trim() != "") { @@ -40,11 +43,16 @@ public void onResume() { infoText += " are now sent to " + this.targetUrl +" in the following format:"; infoText += "

GET " + this.targetUrl + "?sender=<phone#>&msg=<message>

"; - infoText += "If the response body contains text, it will SMSed back to the sender."; + infoText += "If the response body contains text, it will SMS back to the originating phone."; + if (this.polling) { + infoText += "

The target URL will be polled every 15 minutes

"; + } + infoText += "

Press Menu to set SMS identifier or target URL."; infoText += "


Questions/feedback: niryariv@gmail.com"; + // /Home Screen text TextView info = (TextView) this.findViewById(R.id.info); info.setText(Html.fromHtml(infoText)); diff --git a/src/kalsms/niryariv/itp/Prefs.java b/src/kalsms/niryariv/itp/Prefs.java index ab02076..16fa71b 100755 --- a/src/kalsms/niryariv/itp/Prefs.java +++ b/src/kalsms/niryariv/itp/Prefs.java @@ -13,7 +13,6 @@ import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceActivity; -import android.preference.PreferenceManager; import android.util.Log; import android.view.Menu; @@ -54,11 +53,9 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin Intent pintent = new Intent(this, SMSSender.class); PendingIntent pIntent = PendingIntent.getBroadcast(this,0,pintent, 0); if(checkbox.isChecked()) { - long interval = 60*Integer.valueOf(PreferenceManager.getDefaultSharedPreferences(this).getString("pref_poll_interval", "5000"));//5mins;//5mins - long firstPoll = SystemClock.elapsedRealtime() + 60*Integer.valueOf(PreferenceManager.getDefaultSharedPreferences(this).getString("pref_poll_interval", "5000")); - alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, firstPoll, interval, pIntent); - Log.d("KALSMS", "alarm manager turned on "+interval); - }else { + alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), AlarmManager.INTERVAL_FIFTEEN_MINUTES, pIntent); + Log.d("KALSMS", "alarm manager turned on"); + } else { alarm.cancel(pIntent); Log.d("SMS_GATEWAY", "alarm manager turned off"); } From 208945044f3c5855a29464b6cbb7eff65f363c36 Mon Sep 17 00:00:00 2001 From: Nir Yariv Date: Sat, 15 Jan 2011 19:26:40 -0500 Subject: [PATCH 05/69] cleanup home screen text --- src/kalsms/niryariv/itp/Main.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/kalsms/niryariv/itp/Main.java b/src/kalsms/niryariv/itp/Main.java index 1409a95..9188aac 100644 --- a/src/kalsms/niryariv/itp/Main.java +++ b/src/kalsms/niryariv/itp/Main.java @@ -34,7 +34,6 @@ public void onResume() { String infoText = new String(); // Home Screen text - infoText = "All SMS messages"; if (this.identifier.trim() != "") { @@ -46,13 +45,13 @@ public void onResume() { infoText += "If the response body contains text, it will SMS back to the originating phone."; if (this.polling) { - infoText += "

The target URL will be polled every 15 minutes

"; + infoText += "

The target URL will be polled every 15 minutes (note that polling increases power consumption)

"; } infoText += "

Press Menu to set SMS identifier or target URL."; - infoText += "


Questions/feedback: niryariv@gmail.com"; - // /Home Screen text + infoText += "

Questions/feedback: niryariv@gmail.com

"; + // END Home Screen text TextView info = (TextView) this.findViewById(R.id.info); info.setText(Html.fromHtml(infoText)); From 3ed1958ea391cc004d709a18fd2f2bc18a3aa4be Mon Sep 17 00:00:00 2001 From: Nir Yariv Date: Sat, 15 Jan 2011 19:27:19 -0500 Subject: [PATCH 06/69] add a poll=true parameter --- res/xml/prefs.xml | 2 +- src/kalsms/niryariv/itp/SMSReceiver.java | 2 +- src/kalsms/niryariv/itp/SMSSender.java | 2 +- src/kalsms/niryariv/itp/TargetUrlRequest.java | 16 ++++++++++------ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index 2022e3a..b39b361 100644 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -20,7 +20,7 @@ android:key="pref_poll_switch" android:title="Server Polling" android:disableDependentsState="false" - android:summary="Poll target URL every 15 minutes (Note: Polling increased power consumption, and is not supported by all phone models)" + android:summary="Poll target URL every 15 minutes (Note: increases power usage)" > \ No newline at end of file diff --git a/src/kalsms/niryariv/itp/SMSReceiver.java b/src/kalsms/niryariv/itp/SMSReceiver.java index 5394982..265155b 100644 --- a/src/kalsms/niryariv/itp/SMSReceiver.java +++ b/src/kalsms/niryariv/itp/SMSReceiver.java @@ -41,7 +41,7 @@ public void onReceive(Context context, Intent intent) { Log.d("KALSMS", "MSG RCVD:\"" + message + "\" from: " + sender); // send the message to the URL - String resp = url.openURL(sender, message, targetUrl).toString(); + String resp = url.openURL(sender, message, targetUrl, false).toString(); Log.d("KALSMS", "RESP:\"" + resp); // SMS back the response diff --git a/src/kalsms/niryariv/itp/SMSSender.java b/src/kalsms/niryariv/itp/SMSSender.java index 004c629..96940df 100755 --- a/src/kalsms/niryariv/itp/SMSSender.java +++ b/src/kalsms/niryariv/itp/SMSSender.java @@ -26,7 +26,7 @@ public void onReceive(Context context, Intent intent) { Log.d("KALSMS", "url:\"" + targetUrl); TargetUrlRequest url = new TargetUrlRequest(); // send the message to the URL - String resp = url.openURL("","",targetUrl).toString(); + String resp = url.openURL("","",targetUrl, true).toString(); Log.d("KALSMS", "RESP:\"" + resp); diff --git a/src/kalsms/niryariv/itp/TargetUrlRequest.java b/src/kalsms/niryariv/itp/TargetUrlRequest.java index ede5d55..e1e8d92 100644 --- a/src/kalsms/niryariv/itp/TargetUrlRequest.java +++ b/src/kalsms/niryariv/itp/TargetUrlRequest.java @@ -26,15 +26,19 @@ public class TargetUrlRequest { - public String openURL(String sender, String message, String targetUrl) { + public String openURL(String sender, String message, String targetUrl, Boolean isPollRequest) { - String url = targetUrl; + List qparams = new ArrayList(); + if(sender.trim().length() > 0 && message.trim().length() > 0) { - List qparams = new ArrayList(); qparams.add(new BasicNameValuePair("sender", sender)); - qparams.add(new BasicNameValuePair("msg", message)); - url = targetUrl + "?" + URLEncodedUtils.format(qparams, "UTF-8"); - } + qparams.add(new BasicNameValuePair("msg", message)); + } else if (isPollRequest) { + qparams.add(new BasicNameValuePair("poll", "true")); + } + + String url = targetUrl + "?" + URLEncodedUtils.format(qparams, "UTF-8"); + try { HttpClient client = new DefaultHttpClient(); HttpGet get = new HttpGet(url); From 448eafecd8b76d8294e7aa465a97f82a09b21c25 Mon Sep 17 00:00:00 2001 From: Nir Yariv Date: Tue, 18 Jan 2011 13:57:34 -0500 Subject: [PATCH 07/69] Move post-parse sms sending to TargetUrlRequest --- src/kalsms/niryariv/itp/SMSReceiver.java | 15 +----------- src/kalsms/niryariv/itp/SMSSender.java | 16 +------------ src/kalsms/niryariv/itp/TargetUrlRequest.java | 24 ++++++++++++++++++- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/kalsms/niryariv/itp/SMSReceiver.java b/src/kalsms/niryariv/itp/SMSReceiver.java index 265155b..9791d50 100644 --- a/src/kalsms/niryariv/itp/SMSReceiver.java +++ b/src/kalsms/niryariv/itp/SMSReceiver.java @@ -9,7 +9,6 @@ import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; -import android.telephony.SmsManager; import android.telephony.SmsMessage; import android.util.Log; @@ -47,19 +46,7 @@ public void onReceive(Context context, Intent intent) { // SMS back the response if (resp.trim().length() > 0) { ArrayList> items = url.parseXML(resp); - - SmsManager smgr = SmsManager.getDefault(); - for (int j = 0; j < items.size(); j++) { - String sendTo = items.get(j).get(0); - if (sendTo.toLowerCase() == "sender") sendTo = sender; - String sendMsg = items.get(j).get(1); - try { - Log.d("KALSMS", "SEND MSG:\"" + sendMsg + "\" TO: " + sendTo); - smgr.sendTextMessage(sendTo, null, sendMsg, null, null); - } catch (Exception ex) { - Log.d("KALSMS", "SMS FAILED"); - } - } + url.sendMessages(items); } // delete SMS from inbox, to prevent it from filling up DeleteSMSFromInbox(context, mesg); diff --git a/src/kalsms/niryariv/itp/SMSSender.java b/src/kalsms/niryariv/itp/SMSSender.java index 96940df..774084c 100755 --- a/src/kalsms/niryariv/itp/SMSSender.java +++ b/src/kalsms/niryariv/itp/SMSSender.java @@ -7,7 +7,6 @@ import android.content.SharedPreferences; import android.os.PowerManager; import android.preference.PreferenceManager; -import android.telephony.SmsManager; import android.util.Log; public class SMSSender extends BroadcastReceiver { @@ -33,20 +32,7 @@ public void onReceive(Context context, Intent intent) { // SMS back the response if (resp.trim().length() > 0) { ArrayList> items = url.parseXML(resp); - - SmsManager smgr = SmsManager.getDefault(); - - for (int j = 0; j < items.size(); j++) { - String sendTo = items.get(j).get(0); - String sendMsg = items.get(j).get(1); - - try { - Log.d("KALSMS", "SEND MSG:\"" + sendMsg + "\" TO: " + sendTo); - smgr.sendTextMessage(sendTo, null, sendMsg, null, null); - } catch (Exception ex) { - Log.d("KALSMS", "SMS FAILED"); - } - } + url.sendMessages(items); } wake.release(); } diff --git a/src/kalsms/niryariv/itp/TargetUrlRequest.java b/src/kalsms/niryariv/itp/TargetUrlRequest.java index e1e8d92..f891d0e 100644 --- a/src/kalsms/niryariv/itp/TargetUrlRequest.java +++ b/src/kalsms/niryariv/itp/TargetUrlRequest.java @@ -22,12 +22,17 @@ import org.w3c.dom.NodeList; import org.xml.sax.InputSource; +import android.telephony.SmsManager; import android.util.Log; public class TargetUrlRequest { + private String sender = ""; + public String openURL(String sender, String message, String targetUrl, Boolean isPollRequest) { + this.sender = sender; + List qparams = new ArrayList(); if(sender.trim().length() > 0 && message.trim().length() > 0) { @@ -105,5 +110,22 @@ public ArrayList> parseXML(String xml) { e.printStackTrace(); return (output); } - } + } + + public void sendMessages(ArrayList> items) { + SmsManager smgr = SmsManager.getDefault(); + for (int j = 0; j < items.size(); j++) { + String sendTo = items.get(j).get(0); + if (sendTo.toLowerCase() == "sender") sendTo = this.sender; + String sendMsg = items.get(j).get(1); + try { + Log.d("KALSMS", "SEND MSG:\"" + sendMsg + "\" TO: " + sendTo); + smgr.sendTextMessage(sendTo, null, sendMsg, null, null); + } catch (Exception ex) { + Log.d("KALSMS", "SMS FAILED"); + } + } + } + } + From 9473ab1610bc39edc535a2706f396c6d32f6940f Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Sun, 11 Sep 2011 01:35:10 -0700 Subject: [PATCH 08/69] use POST in http requests; use different url for outgoing sms; poll for outgoing sms more frequently; notify server when sms messages are sent --- AndroidManifest.xml | 28 +-- build.properties | 17 ++ build.xml | 79 +++++++++ local.properties | 10 ++ proguard.cfg | 40 +++++ res/layout/main.xml | 12 +- res/values/strings.xml | 3 +- res/xml/prefs.xml | 43 ++--- src/kalsms/niryariv/itp/Main.java | 91 ---------- src/kalsms/niryariv/itp/Prefs.java | 77 --------- src/kalsms/niryariv/itp/SMSReceiver.java | 96 ----------- src/kalsms/niryariv/itp/SMSSender.java | 39 ----- src/kalsms/niryariv/itp/TargetUrlRequest.java | 131 -------------- src/org/envaya/kalsms/App.java | 158 +++++++++++++++++ src/org/envaya/kalsms/DBHelper.java | 37 ++++ .../kalsms/IncomingMessageForwarder.java | 161 ++++++++++++++++++ src/org/envaya/kalsms/Main.java | 102 +++++++++++ .../envaya/kalsms/MessageStatusNotifier.java | 82 +++++++++ .../envaya/kalsms/OutgoingMessagePoller.java | 89 ++++++++++ src/org/envaya/kalsms/OutgoingSmsMessage.java | 60 +++++++ src/org/envaya/kalsms/Prefs.java | 71 ++++++++ 21 files changed, 952 insertions(+), 474 deletions(-) mode change 100644 => 100755 AndroidManifest.xml create mode 100755 build.properties create mode 100755 build.xml create mode 100755 local.properties create mode 100755 proguard.cfg mode change 100644 => 100755 res/layout/main.xml mode change 100644 => 100755 res/values/strings.xml mode change 100644 => 100755 res/xml/prefs.xml delete mode 100644 src/kalsms/niryariv/itp/Main.java delete mode 100755 src/kalsms/niryariv/itp/Prefs.java delete mode 100644 src/kalsms/niryariv/itp/SMSReceiver.java delete mode 100755 src/kalsms/niryariv/itp/SMSSender.java delete mode 100644 src/kalsms/niryariv/itp/TargetUrlRequest.java create mode 100755 src/org/envaya/kalsms/App.java create mode 100755 src/org/envaya/kalsms/DBHelper.java create mode 100755 src/org/envaya/kalsms/IncomingMessageForwarder.java create mode 100755 src/org/envaya/kalsms/Main.java create mode 100755 src/org/envaya/kalsms/MessageStatusNotifier.java create mode 100755 src/org/envaya/kalsms/OutgoingMessagePoller.java create mode 100755 src/org/envaya/kalsms/OutgoingSmsMessage.java create mode 100755 src/org/envaya/kalsms/Prefs.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml old mode 100644 new mode 100755 index 7841597..7de0f06 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,6 +1,6 @@ @@ -19,18 +19,24 @@
- - - - - + + + + + - - + + - - + + + + + + + +
\ No newline at end of file diff --git a/build.properties b/build.properties new file mode 100755 index 0000000..ee52d86 --- /dev/null +++ b/build.properties @@ -0,0 +1,17 @@ +# This file is used to override default values used by the Ant build system. +# +# This file must be checked in Version Control Systems, as it is +# integral to the build system of your project. + +# This file is only used by the Ant script. + +# You can use this to override default values such as +# 'source.dir' for the location of your java source folder and +# 'out.dir' for the location of your output folder. + +# You can also use it define how the release builds are signed by declaring +# the following properties: +# 'key.store' for the location of your keystore and +# 'key.alias' for the name of the key to use. +# The password will be asked during the build when you use the 'release' target. + diff --git a/build.xml b/build.xml new file mode 100755 index 0000000..6f2493d --- /dev/null +++ b/build.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local.properties b/local.properties new file mode 100755 index 0000000..407948f --- /dev/null +++ b/local.properties @@ -0,0 +1,10 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must *NOT* be checked in Version Control Systems, +# as it contains information specific to your local configuration. + +# location of the SDK. This is only used by Ant +# For customization when using a Version Control System, please read the +# header note. +sdk.dir=C:\\android-sdk diff --git a/proguard.cfg b/proguard.cfg new file mode 100755 index 0000000..f0b04dc --- /dev/null +++ b/proguard.cfg @@ -0,0 +1,40 @@ +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* + +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference +-keep public class com.android.vending.licensing.ILicensingService + +-keepclasseswithmembernames class * { + native ; +} + +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet); +} + +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet, int); +} + +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} diff --git a/res/layout/main.xml b/res/layout/main.xml old mode 100644 new mode 100755 index 22272f0..634ae00 --- a/res/layout/main.xml +++ b/res/layout/main.xml @@ -3,11 +3,11 @@ android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#333333"> - - - + android:layout_height="fill_parent" + android:id="@+id/info" + android:textColor="#FFFFFF" + android:layout_margin="5px"> diff --git a/res/values/strings.xml b/res/values/strings.xml old mode 100644 new mode 100755 index a734339..b48bf11 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,5 +1,4 @@ - SMS Gateway Running.\n - KalSMS + KalSMS Envaya diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml old mode 100644 new mode 100755 index b39b361..e3d2c3c --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -1,26 +1,27 @@ + - - - - - - + + + + + \ No newline at end of file diff --git a/src/kalsms/niryariv/itp/Main.java b/src/kalsms/niryariv/itp/Main.java deleted file mode 100644 index 9188aac..0000000 --- a/src/kalsms/niryariv/itp/Main.java +++ /dev/null @@ -1,91 +0,0 @@ -package kalsms.niryariv.itp; - -import kalsms.niryariv.itp.R; -import android.app.Activity; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Html; -import android.util.Log; -import android.view.Menu; -import android.widget.TextView; - -public class Main extends Activity { - -// public static final String PREFS_NAME = "KalPrefsFile"; - - public String identifier = ""; - public String targetUrl = ""; - public Boolean polling = false; - - public void onResume() { - Log.d("KALSMS", "RESUME"); - super.onResume(); - - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); - - this.identifier = settings.getString("pref_identifier", ""); - this.targetUrl = settings.getString("pref_target_url", ""); - this.polling = settings.getBoolean("pref_poll_switch", false); - - Log.d("KALSMS", "onResume ident:" + this.identifier +"\ntarget:" + this.targetUrl); - - String infoText = new String(); - - // Home Screen text - infoText = "All SMS messages"; - - if (this.identifier.trim() != "") { - infoText += " starting with " + this.identifier + ""; - } - - infoText += " are now sent to " + this.targetUrl +" in the following format:"; - infoText += "

GET " + this.targetUrl + "?sender=<phone#>&msg=<message>

"; - infoText += "If the response body contains text, it will SMS back to the originating phone."; - - if (this.polling) { - infoText += "

The target URL will be polled every 15 minutes (note that polling increases power consumption)

"; - } - - infoText += "

Press Menu to set SMS identifier or target URL."; - - infoText += "

Questions/feedback: niryariv@gmail.com

"; - // END Home Screen text - - TextView info = (TextView) this.findViewById(R.id.info); - info.setText(Html.fromHtml(infoText)); - - } - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.main); - PreferenceManager.setDefaultValues(this, R.xml.prefs, false); - - Log.d("KALSMS", "STARTED"); - } - - - // first time the Menu key is pressed - public boolean onCreateOptionsMenu(Menu menu) { - startActivity(new Intent(this, Prefs.class)); - return(true); - } - - // any other time the Menu key is pressed - public boolean onPrepareOptionsMenu(Menu menu) { - startActivity(new Intent(this, Prefs.class)); - return(true); - } - - - @Override - protected void onStop(){ - // dont do much with this, atm.. - super.onStop(); - } - -} diff --git a/src/kalsms/niryariv/itp/Prefs.java b/src/kalsms/niryariv/itp/Prefs.java deleted file mode 100755 index 16fa71b..0000000 --- a/src/kalsms/niryariv/itp/Prefs.java +++ /dev/null @@ -1,77 +0,0 @@ -package kalsms.niryariv.itp; - -import kalsms.niryariv.itp.R; -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.os.Bundle; -import android.os.SystemClock; -import android.preference.CheckBoxPreference; -import android.preference.EditTextPreference; -import android.preference.Preference; -import android.preference.PreferenceActivity; -import android.util.Log; -import android.view.Menu; - - -public class Prefs extends PreferenceActivity implements OnSharedPreferenceChangeListener { - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.prefs); - } - - protected void onResume() { - super.onResume(); - // Set up a listener whenever a key changes - getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); - } - - @Override - protected void onPause() { - super.onPause(); - // Unregister the listener whenever a key changes - getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); - } - - - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - Preference pref = findPreference(key); - if (pref instanceof EditTextPreference) { - EditTextPreference textPref = (EditTextPreference) pref; - pref.setSummary(textPref.getSummary()); - Log.d("KALSMS", "textPref.getSummary(): " + textPref.getSummary()); - } - if(pref instanceof CheckBoxPreference) { - CheckBoxPreference checkbox = (CheckBoxPreference) pref; - AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - Intent pintent = new Intent(this, SMSSender.class); - PendingIntent pIntent = PendingIntent.getBroadcast(this,0,pintent, 0); - if(checkbox.isChecked()) { - alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), AlarmManager.INTERVAL_FIFTEEN_MINUTES, pIntent); - Log.d("KALSMS", "alarm manager turned on"); - } else { - alarm.cancel(pIntent); - Log.d("SMS_GATEWAY", "alarm manager turned off"); - } - } - } - - // first time the Menu key is pressed - public boolean onCreateOptionsMenu(Menu menu) { - startActivity(new Intent(this, Prefs.class)); - return(true); - } - - // any other time the Menu key is pressed - public boolean onPrepareOptionsMenu(Menu menu) { - startActivity(new Intent(this, Prefs.class)); - return(true); - } -} - diff --git a/src/kalsms/niryariv/itp/SMSReceiver.java b/src/kalsms/niryariv/itp/SMSReceiver.java deleted file mode 100644 index 9791d50..0000000 --- a/src/kalsms/niryariv/itp/SMSReceiver.java +++ /dev/null @@ -1,96 +0,0 @@ -package kalsms.niryariv.itp; - -import java.util.ArrayList; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.telephony.SmsMessage; -import android.util.Log; - -public class SMSReceiver extends BroadcastReceiver { - - @Override - // source: http://www.devx.com/wireless/Article/39495/1954 - public void onReceive(Context context, Intent intent) { - if (!intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")) { - return; - } - - // get settings - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); - TargetUrlRequest url = new TargetUrlRequest(); - - String identifier = settings.getString("pref_identifier", ""); - String targetUrl = settings.getString("pref_target_url", ""); - - SmsMessage msgs[] = getMessagesFromIntent(intent); - - for (int i = 0; i < msgs.length; i++) { - SmsMessage mesg = msgs[i]; - String message = mesg.getDisplayMessageBody(); - String sender = mesg.getDisplayOriginatingAddress(); - - if (message != null && message.length() > 0 - && (message.toLowerCase().startsWith(identifier) || identifier.trim() == "")) { - Log.d("KALSMS", "MSG RCVD:\"" + message + "\" from: " + sender); - - // send the message to the URL - String resp = url.openURL(sender, message, targetUrl, false).toString(); - Log.d("KALSMS", "RESP:\"" + resp); - - // SMS back the response - if (resp.trim().length() > 0) { - ArrayList> items = url.parseXML(resp); - url.sendMessages(items); - } - // delete SMS from inbox, to prevent it from filling up - DeleteSMSFromInbox(context, mesg); - } - } - } - - private void DeleteSMSFromInbox(Context context, SmsMessage mesg) { - Log.d("KALSMS", "try to delete SMS"); - try { - Uri uriSms = Uri.parse("content://sms/inbox"); - StringBuilder sb = new StringBuilder(); - sb.append("address='" + mesg.getOriginatingAddress() + "' AND "); - sb.append("body='" + mesg.getMessageBody() + "'"); - Cursor c = context.getContentResolver().query(uriSms, null, sb.toString(), null, null); - c.moveToFirst(); - int thread_id = c.getInt(1); - context.getContentResolver().delete(Uri.parse("content://sms/conversations/" + thread_id), null, null); - c.close(); - } catch (Exception ex) { - // deletions don't work most of the time since the timing of the - // receipt and saving to the inbox - // makes it difficult to match up perfectly. the SMS might not be in - // the inbox yet when this receiver triggers! - Log.d("SmsReceiver", "Error deleting sms from inbox: " + ex.getMessage()); - } - } - - - // from http://github.com/dimagi/rapidandroid - // source: http://www.devx.com/wireless/Article/39495/1954 - private SmsMessage[] getMessagesFromIntent(Intent intent) { - SmsMessage retMsgs[] = null; - Bundle bdl = intent.getExtras(); - try { - Object pdus[] = (Object[]) bdl.get("pdus"); - retMsgs = new SmsMessage[pdus.length]; - for (int n = 0; n < pdus.length; n++) { - byte[] byteData = (byte[]) pdus[n]; - retMsgs[n] = SmsMessage.createFromPdu(byteData); - } - } catch (Exception e) { - Log.e("KALSMS", "GetMessages ERROR\n" + e); - } - return retMsgs; - } -} \ No newline at end of file diff --git a/src/kalsms/niryariv/itp/SMSSender.java b/src/kalsms/niryariv/itp/SMSSender.java deleted file mode 100755 index 774084c..0000000 --- a/src/kalsms/niryariv/itp/SMSSender.java +++ /dev/null @@ -1,39 +0,0 @@ -package kalsms.niryariv.itp; - -import java.util.ArrayList; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.PowerManager; -import android.preference.PreferenceManager; -import android.util.Log; - -public class SMSSender extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - - // acquiring the wake clock to prevent device from sleeping while request is processed - final PowerManager pm = (PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE); - PowerManager.WakeLock wake = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "http_request"); - wake.acquire(); - - // get settings - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context); - String targetUrl = settings.getString("pref_target_url", ""); - Log.d("KALSMS", "url:\"" + targetUrl); - TargetUrlRequest url = new TargetUrlRequest(); - // send the message to the URL - String resp = url.openURL("","",targetUrl, true).toString(); - - Log.d("KALSMS", "RESP:\"" + resp); - - // SMS back the response - if (resp.trim().length() > 0) { - ArrayList> items = url.parseXML(resp); - url.sendMessages(items); - } - wake.release(); - } -} diff --git a/src/kalsms/niryariv/itp/TargetUrlRequest.java b/src/kalsms/niryariv/itp/TargetUrlRequest.java deleted file mode 100644 index f891d0e..0000000 --- a/src/kalsms/niryariv/itp/TargetUrlRequest.java +++ /dev/null @@ -1,131 +0,0 @@ -package kalsms.niryariv.itp; - -import java.io.StringReader; -import java.util.ArrayList; -import java.util.List; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.utils.URLEncodedUtils; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.message.BasicNameValuePair; -import org.apache.http.util.EntityUtils; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import android.telephony.SmsManager; -import android.util.Log; - -public class TargetUrlRequest { - - private String sender = ""; - - public String openURL(String sender, String message, String targetUrl, Boolean isPollRequest) { - - this.sender = sender; - - List qparams = new ArrayList(); - - if(sender.trim().length() > 0 && message.trim().length() > 0) { - qparams.add(new BasicNameValuePair("sender", sender)); - qparams.add(new BasicNameValuePair("msg", message)); - } else if (isPollRequest) { - qparams.add(new BasicNameValuePair("poll", "true")); - } - - String url = targetUrl + "?" + URLEncodedUtils.format(qparams, "UTF-8"); - - try { - HttpClient client = new DefaultHttpClient(); - HttpGet get = new HttpGet(url); - - HttpResponse responseGet = client.execute(get); - HttpEntity resEntityGet = responseGet.getEntity(); - if (resEntityGet != null) { - String resp = EntityUtils.toString(resEntityGet); - Log.e("KALSMS", "HTTP RESP" + resp); - return resp; - } - } catch (Exception e) { - Log.e("KALSMS", "HTTP REQ FAILED:" + url); - e.printStackTrace(); - } - return ""; - } - - public ArrayList> parseXML(String xml) { - ArrayList> output = new ArrayList>(); - - try { - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - - Document doc = dBuilder.parse(new InputSource(new StringReader(xml))); - - NodeList rnodes = doc.getElementsByTagName("reply"); - - NodeList nodes = rnodes.item(0).getChildNodes(); - - for (int i=0; i < nodes.getLength(); i++) { - try { - List item = new ArrayList(); - - Node node = nodes.item(i); - if (node.getNodeType() != Node.ELEMENT_NODE) continue; - - Element e = (Element) node; - String nodeName = e.getNodeName(); - - if (nodeName.equalsIgnoreCase("sms")) { - if (!e.getAttribute("phone").equals("")) { - item.add(e.getAttribute("phone")); - item.add(e.getFirstChild().getNodeValue()); - output.add((ArrayList) item); - } - } else if (nodeName.equalsIgnoreCase("sms-to-sender")) { - item.add("sender"); - item.add(e.getFirstChild().getNodeValue()); - output.add((ArrayList) item); - } else { - continue; - } - } catch (Exception e){ - Log.e("KALSMS", "FAILED PARSING XML NODE# " + i ); - } - } - Log.e("KALSMS", "PARSING XML RETURNS " + output ); - return (output); - - } catch (Exception e) { - Log.e("KALSMS", "PARSING XML FAILED: " + xml ); - e.printStackTrace(); - return (output); - } - } - - public void sendMessages(ArrayList> items) { - SmsManager smgr = SmsManager.getDefault(); - for (int j = 0; j < items.size(); j++) { - String sendTo = items.get(j).get(0); - if (sendTo.toLowerCase() == "sender") sendTo = this.sender; - String sendMsg = items.get(j).get(1); - try { - Log.d("KALSMS", "SEND MSG:\"" + sendMsg + "\" TO: " + sendTo); - smgr.sendTextMessage(sendTo, null, sendMsg, null, null); - } catch (Exception ex) { - Log.d("KALSMS", "SMS FAILED"); - } - } - } - -} - diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java new file mode 100755 index 0000000..97866ed --- /dev/null +++ b/src/org/envaya/kalsms/App.java @@ -0,0 +1,158 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.envaya.kalsms; + +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.preference.PreferenceManager; +import android.telephony.SmsManager; +import android.util.Log; + +/** + * + * @author Jesse + */ +public class App { + + public static final int OUTGOING_POLL_SECONDS = 30; + + 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"; + + public Context context; + public SharedPreferences settings; + + public App(Context context) + { + this.context = context; + this.settings = PreferenceManager.getDefaultSharedPreferences(context); + } + + static void debug(String msg) + { + Log.d(LOG_NAME, msg); + } + + public void log(String msg) + { + Log.d(LOG_NAME, msg); + + Intent broadcast = new Intent(App.LOG_INTENT); + broadcast.putExtra("message", msg); + context.sendBroadcast(broadcast); + } + + public void logError(Throwable ex) + { + logError("ERROR", ex); + } + + public void logError(String msg, Throwable ex) + { + logError(msg, ex, false); + } + + public void logError(String msg, Throwable ex, boolean detail) + { + log(msg + ": " + ex.getClass().getName() + ": " + ex.getMessage()); + + if (detail) + { + for (StackTraceElement elem : ex.getStackTrace()) + { + log(elem.getClassName() + ":" + elem.getMethodName() + ":" + elem.getLineNumber()); + } + Throwable innerEx = ex.getCause(); + if (innerEx != null) + { + logError("Inner exception:", innerEx, true); + } + } + } + + public String getIncomingUrl() + { + return getServerUrl() + "/pg/receive_sms"; + } + + public String getOutgoingUrl() + { + return getServerUrl() + "/pg/dequeue_sms"; + } + + public String getSendStatusUrl() + { + return getServerUrl() + "/pg/sms_sent"; + } + + public String getServerUrl() + { + return settings.getString("server_url", ""); + } + + public String getPhoneNumber() + { + return settings.getString("phone_number", ""); + } + + public String getPassword() + { + return settings.getString("password", ""); + } + + private SQLiteDatabase db; + public SQLiteDatabase getWritableDatabase() + { + if (db == null) + { + db = new DBHelper(context).getWritableDatabase(); + } + return db; + } + + public void sendSMS(OutgoingSmsMessage sms) + { + String serverId = sms.getServerId(); + + if (serverId != null) + { + SQLiteDatabase db = this.getWritableDatabase(); + Cursor cursor = + db.rawQuery("select 1 from sent_sms where server_id=?", new String[] { serverId }); + + 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); + db.insert("sent_sms", null, values); + } + + SmsManager smgr = SmsManager.getDefault(); + + Intent intent = new Intent(App.SEND_STATUS_INTENT); + intent.putExtra("serverId", serverId); + + 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); + } +} diff --git a/src/org/envaya/kalsms/DBHelper.java b/src/org/envaya/kalsms/DBHelper.java new file mode 100755 index 0000000..0710128 --- /dev/null +++ b/src/org/envaya/kalsms/DBHelper.java @@ -0,0 +1,37 @@ +/* + * 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 = 1; + private static final String DATABASE_NAME = "org.envaya.kalsms.db"; + + private static final String SENT_SMS_TABLE_CREATE = + "CREATE TABLE sent_sms (server_id text);" + + "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(SENT_SMS_TABLE_CREATE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + } +} diff --git a/src/org/envaya/kalsms/IncomingMessageForwarder.java b/src/org/envaya/kalsms/IncomingMessageForwarder.java new file mode 100755 index 0000000..3eb2727 --- /dev/null +++ b/src/org/envaya/kalsms/IncomingMessageForwarder.java @@ -0,0 +1,161 @@ +package org.envaya.kalsms; + +import android.app.Activity; +import android.app.PendingIntent; +import java.util.ArrayList; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.telephony.SmsManager; +import android.telephony.SmsMessage; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.http.HttpResponse; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import org.apache.http.NameValuePair; +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; + +public class IncomingMessageForwarder extends BroadcastReceiver { + + private App app; + + public List sendMessageToServer(SmsMessage sms) { + + String message = sms.getDisplayMessageBody(); + String sender = sms.getDisplayOriginatingAddress(); + String recipient = app.getPhoneNumber(); + + app.log("Received SMS from " + sender); + + if (message == null || message.length() == 0) { + return new ArrayList(); + } + + List replies = new ArrayList(); + + try { + + List params = new ArrayList(); + params.add(new BasicNameValuePair("from", sender)); + params.add(new BasicNameValuePair("to", recipient)); + params.add(new BasicNameValuePair("message", message)); + params.add(new BasicNameValuePair("secret", app.getPassword())); + + HttpClient client = new DefaultHttpClient(); + HttpPost post = new HttpPost(app.getIncomingUrl()); + post.setEntity(new UrlEncodedFormEntity(params)); + + app.log("Forwarding incoming SMS to server"); + + HttpResponse response = client.execute(post); + + InputStream responseStream = response.getEntity().getContent(); + DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document xml = xmlBuilder.parse(responseStream); + + NodeList smsNodes = xml.getElementsByTagName("Sms"); + for (int i = 0; i < smsNodes.getLength(); i++) { + Element smsElement = (Element) smsNodes.item(i); + + OutgoingSmsMessage reply = new OutgoingSmsMessage(); + + reply.setFrom(recipient); + reply.setTo(sender); + reply.setMessage(smsElement.getFirstChild().getNodeValue()); + + replies.add(reply); + } + } catch (SAXException ex) { + app.logError("Error parsing response from server while forwarding incoming message", ex); + } catch (IOException ex) { + app.logError("Error forwarding incoming message to server", ex); + } catch (ParserConfigurationException ex) { + app.logError("Error configuring XML parser", ex); + } + + return replies; + } + + public void smsReceived(Intent intent) { + + for (SmsMessage sms : getMessagesFromIntent(intent)) { + List replies = sendMessageToServer(sms); + + for (OutgoingSmsMessage reply : replies) + { + app.sendSMS(reply); + } + + //DeleteSMSFromInbox(context, mesg); + } + + } + + + @Override + // source: http://www.devx.com/wireless/Article/39495/1954 + public void onReceive(Context context, Intent intent) { + try { + this.app = new App(context); + + String action = intent.getAction(); + + if (action.equals("android.provider.Telephony.SMS_RECEIVED")) { + smsReceived(intent); + } + } catch (Throwable ex) { + app.logError("Unexpected error in IncomingMessageForwarder", ex, true); + } + } + + /* + private void DeleteSMSFromInbox(Context context, SmsMessage mesg) { + Log.d("KALSMS", "try to delete SMS"); + try { + Uri uriSms = Uri.parse("content://sms/inbox"); + StringBuilder sb = new StringBuilder(); + sb.append("address='" + mesg.getOriginatingAddress() + "' AND "); + sb.append("body='" + mesg.getMessageBody() + "'"); + Cursor c = context.getContentResolver().query(uriSms, null, sb.toString(), null, null); + c.moveToFirst(); + int thread_id = c.getInt(1); + context.getContentResolver().delete(Uri.parse("content://sms/conversations/" + thread_id), null, null); + c.close(); + } catch (Exception ex) { + // deletions don't work most of the time since the timing of the + // receipt and saving to the inbox + // makes it difficult to match up perfectly. the SMS might not be in + // the inbox yet when this receiver triggers! + Log.d("SmsReceiver", "Error deleting sms from inbox: " + ex.getMessage()); + } + } + */ + + // from http://github.com/dimagi/rapidandroid + // source: http://www.devx.com/wireless/Article/39495/1954 + + private SmsMessage[] getMessagesFromIntent(Intent intent) { + SmsMessage retMsgs[] = null; + Bundle bdl = intent.getExtras(); + Object pdus[] = (Object[]) bdl.get("pdus"); + retMsgs = new SmsMessage[pdus.length]; + for (int n = 0; n < pdus.length; n++) { + byte[] byteData = (byte[]) pdus[n]; + retMsgs[n] = SmsMessage.createFromPdu(byteData); + } + return retMsgs; + } +} \ No newline at end of file diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java new file mode 100755 index 0000000..a4e4e8f --- /dev/null +++ b/src/org/envaya/kalsms/Main.java @@ -0,0 +1,102 @@ +package org.envaya.kalsms; + +import android.app.Activity; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.text.Html; +import android.text.method.ScrollingMovementMethod; +import android.view.Menu; +import android.widget.TextView; + +public class Main extends Activity { + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + showLogMessage(intent.getExtras().getString("message")); + } + }; + + public void showLogMessage(String message) + { + TextView info = (TextView) Main.this.findViewById(R.id.info); + if (message != null) + { + info.append(message + "\n"); + } + } + + 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"); + + setContentView(R.layout.main); + PreferenceManager.setDefaultValues(this, R.xml.prefs, false); + + AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, + 0, + new Intent(this, OutgoingMessagePoller.class), + 0); + + alarm.setRepeating( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime(), + App.OUTGOING_POLL_SECONDS * 1000, + pendingIntent); + + App app = new App(this.getApplication()); + + TextView info = (TextView) this.findViewById(R.id.info); + + info.setText(Html.fromHtml("SMS Gateway running.
")); + + showLogMessage("Server URL is: " + app.getServerUrl()); + showLogMessage("Your phone number is: " + app.getPhoneNumber()); + showLogMessage("Checking for outgoing messages every " + App.OUTGOING_POLL_SECONDS + " sec"); + + info.append(Html.fromHtml("Press Menu to edit settings.
")); + + info.setMovementMethod(new ScrollingMovementMethod()); + + IntentFilter logReceiverFilter = new IntentFilter(); + + logReceiverFilter.addAction(App.LOG_INTENT); + registerReceiver(logReceiver, logReceiverFilter); + } + + // first time the Menu key is pressed + public boolean onCreateOptionsMenu(Menu menu) { + startActivity(new Intent(this, Prefs.class)); + return(true); + } + + // any other time the Menu key is pressed + public boolean onPrepareOptionsMenu(Menu menu) { + startActivity(new Intent(this, Prefs.class)); + return(true); + } + + @Override + protected void onStop(){ + // dont do much with this, atm.. + super.onStop(); + } + +} \ No newline at end of file diff --git a/src/org/envaya/kalsms/MessageStatusNotifier.java b/src/org/envaya/kalsms/MessageStatusNotifier.java new file mode 100755 index 0000000..a2bad10 --- /dev/null +++ b/src/org/envaya/kalsms/MessageStatusNotifier.java @@ -0,0 +1,82 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +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 java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +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; + +public class MessageStatusNotifier extends BroadcastReceiver { + + private App app; + + public void notifySuccess(String serverId) + { + if (serverId != null) + { + try { + app.log("Notifying server of sent SMS id=" + serverId); + List params = new ArrayList(); + params.add(new BasicNameValuePair("from", app.getPhoneNumber())); + params.add(new BasicNameValuePair("secret", app.getPassword())); + params.add(new BasicNameValuePair("id", serverId)); + + HttpClient client = new DefaultHttpClient(); + HttpPost post = new HttpPost(app.getSendStatusUrl()); + post.setEntity(new UrlEncodedFormEntity(params)); + + client.execute(post); + } + catch (IOException ex) + { + app.logError("Error while notifying server of outgoing message", ex); + } + } + } + + @Override + public void onReceive(Context context, Intent intent) { + app = new App(context); + + String serverId = intent.getExtras().getString("serverId"); + + String desc = serverId == null ? "SMS reply" : ("SMS id=" + serverId); + + switch (getResultCode()) { + case Activity.RESULT_OK: + app.log(desc + " sent successfully"); + this.notifySuccess(serverId); + break; + case SmsManager.RESULT_ERROR_GENERIC_FAILURE: + app.log(desc + " could not be sent (generic failure)"); + break; + case SmsManager.RESULT_ERROR_RADIO_OFF: + app.log(desc + " could not be sent (radio off)"); + break; + case SmsManager.RESULT_ERROR_NO_SERVICE: + app.log(desc + " could not be sent (no service)"); + break; + case SmsManager.RESULT_ERROR_NULL_PDU: + app.log(desc + " could not be sent (null PDU"); + break; + default: + app.log("SMS could not be sent (unknown error)"); + break; + } + } +} diff --git a/src/org/envaya/kalsms/OutgoingMessagePoller.java b/src/org/envaya/kalsms/OutgoingMessagePoller.java new file mode 100755 index 0000000..f204cac --- /dev/null +++ b/src/org/envaya/kalsms/OutgoingMessagePoller.java @@ -0,0 +1,89 @@ +package org.envaya.kalsms; + +import java.util.ArrayList; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +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.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +public class OutgoingMessagePoller extends BroadcastReceiver { + + private App app; + + @Override + public void onReceive(Context context, Intent intent) { + try + { + app = new App(context); + + app.log("Checking for outgoing messages"); + + for (OutgoingSmsMessage sms : getOutgoingMessages()) + { + app.sendSMS(sms); + } + } + catch (Throwable ex) + { + app.logError("Unexpected error in OutgoingMessagePoller", ex, true); + } + } + + public List getOutgoingMessages() { + List messages = new ArrayList(); + + try { + List params = new ArrayList(); + params.add(new BasicNameValuePair("from", app.getPhoneNumber())); + params.add(new BasicNameValuePair("secret", app.getPassword())); + + HttpClient client = new DefaultHttpClient(); + HttpPost post = new HttpPost(app.getOutgoingUrl()); + post.setEntity(new UrlEncodedFormEntity(params)); + + HttpResponse response = client.execute(post); + + InputStream responseStream = response.getEntity().getContent(); + DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document xml = xmlBuilder.parse(responseStream); + + NodeList smsNodes = xml.getElementsByTagName("Sms"); + for (int i = 0; i < smsNodes.getLength(); i++) { + Element smsElement = (Element) smsNodes.item(i); + OutgoingSmsMessage sms = new OutgoingSmsMessage(); + + sms.setFrom(app.getPhoneNumber()); + sms.setTo(smsElement.getAttribute("to")); + sms.setMessage(smsElement.getFirstChild().getNodeValue()); + sms.setServerId(smsElement.getAttribute("id")); + + messages.add(sms); + } + } catch (SAXException ex) { + app.logError("Error parsing response from server while retreiving outgoing messages", ex); + } catch (IOException ex) { + app.logError("Error retreiving outgoing messages from server", ex); + } catch (ParserConfigurationException ex) { + app.logError("Error configuring XML parser", ex); + } + + return messages; + } + +} diff --git a/src/org/envaya/kalsms/OutgoingSmsMessage.java b/src/org/envaya/kalsms/OutgoingSmsMessage.java new file mode 100755 index 0000000..bed824a --- /dev/null +++ b/src/org/envaya/kalsms/OutgoingSmsMessage.java @@ -0,0 +1,60 @@ + +package org.envaya.kalsms; + +public class OutgoingSmsMessage { + + private String serverId; + private String message; + private String from; + private String to; + + public OutgoingSmsMessage() + { + } + + public String getLogName() + { + return (serverId == null) ? "SMS reply" : ("SMS id=" + serverId); + } + + public String getServerId() + { + return serverId; + } + + public void setServerId(String id) + { + this.serverId = id; + } + + public String getMessage() + { + return message; + } + + public void setMessage(String message) + { + this.message = message; + } + + public String getFrom() + { + return from; + } + + public void setFrom(String from) + { + this.from = from; + } + + public String getTo() + { + return to; + } + + public void setTo(String to) + { + this.to = to; + } + +} diff --git a/src/org/envaya/kalsms/Prefs.java b/src/org/envaya/kalsms/Prefs.java new file mode 100755 index 0000000..4d00416 --- /dev/null +++ b/src/org/envaya/kalsms/Prefs.java @@ -0,0 +1,71 @@ +package org.envaya.kalsms; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.view.Menu; + +public class Prefs extends PreferenceActivity implements OnSharedPreferenceChangeListener { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.prefs); + } + + @Override + protected void onResume() { + super.onResume(); + // Set up a listener whenever a key changes + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + protected void onPause() { + super.onPause(); + // Unregister the listener whenever a key changes + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } + + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + /* + Preference pref = findPreference(key); + if (pref instanceof EditTextPreference) { + EditTextPreference textPref = (EditTextPreference) pref; + pref.setSummary(textPref.getSummary()); + Log.d("KALSMS", "textPref.getSummary(): " + textPref.getSummary()); + } + if(pref instanceof CheckBoxPreference) { + CheckBoxPreference checkbox = (CheckBoxPreference) pref; + AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent pintent = new Intent(this, SMSSender.class); + PendingIntent pIntent = PendingIntent.getBroadcast(this,0,pintent, 0); + if(checkbox.isChecked()) { + alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime(), + AlarmManager.INTERVAL_FIFTEEN_MINUTES, pIntent); + Log.d("KALSMS", "alarm manager turned on"); + } else { + alarm.cancel(pIntent); + Log.d("SMS_GATEWAY", "alarm manager turned off"); + } + } + */ + } + + // first time the Menu key is pressed + @Override + public boolean onCreateOptionsMenu(Menu menu) { + startActivity(new Intent(this, Prefs.class)); + return (true); + } + + // any other time the Menu key is pressed + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + startActivity(new Intent(this, Prefs.class)); + return (true); + } +} From fc6cda4ea32620ab0cd9052c4fb6b9501bf95ea7 Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Sun, 11 Sep 2011 20:59:51 -0700 Subject: [PATCH 09/69] do http requests in AsyncTask so they don't freeze the UI thread; improve log messages; make poll interval configurable --- AndroidManifest.xml | 9 +- default.properties | 2 +- local.properties | 10 - res/layout/main.xml | 5 +- res/values/arrays.xml | 27 +++ res/xml/prefs.xml | 16 +- src/org/envaya/kalsms/App.java | 117 ++++++++--- src/org/envaya/kalsms/DBHelper.java | 16 +- src/org/envaya/kalsms/HttpTask.java | 181 ++++++++++++++++++ .../kalsms/IncomingMessageForwarder.java | 164 +++++++--------- src/org/envaya/kalsms/Main.java | 81 +++++--- .../envaya/kalsms/MessageStatusNotifier.java | 74 ++++--- .../envaya/kalsms/OutgoingMessagePoller.java | 97 +++------- src/org/envaya/kalsms/Prefs.java | 95 ++++++--- 14 files changed, 579 insertions(+), 315 deletions(-) delete mode 100755 local.properties create mode 100755 res/values/arrays.xml create mode 100755 src/org/envaya/kalsms/HttpTask.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7de0f06..023fbb9 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -23,10 +23,7 @@ - - - - + @@ -34,9 +31,11 @@ + + + - \ No newline at end of file diff --git a/default.properties b/default.properties index 9d6f70d..4735723 100644 --- a/default.properties +++ b/default.properties @@ -8,6 +8,6 @@ # project structure. # Project target. -target=android-4 +target=android-8 # Indicates whether an apk should be generated for each density. split.density=false diff --git a/local.properties b/local.properties deleted file mode 100755 index 407948f..0000000 --- a/local.properties +++ /dev/null @@ -1,10 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must *NOT* be checked in Version Control Systems, -# as it contains information specific to your local configuration. - -# location of the SDK. This is only used by Ant -# For customization when using a Version Control System, please read the -# header note. -sdk.dir=C:\\android-sdk diff --git a/res/layout/main.xml b/res/layout/main.xml index 634ae00..61c6b84 100755 --- a/res/layout/main.xml +++ b/res/layout/main.xml @@ -3,11 +3,14 @@ android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#333333"> + + + \ No newline at end of file diff --git a/res/values/arrays.xml b/res/values/arrays.xml new file mode 100755 index 0000000..49f28f1 --- /dev/null +++ b/res/values/arrays.xml @@ -0,0 +1,27 @@ + + + + + 15 sec + 30 sec + 1 minute + 2 minutes + 5 minutes + 10 minutes + 30 minutes + 1 hour + never + + + + 15 + 30 + 60 + 120 + 300 + 600 + 1800 + 3600 + 0 + + \ No newline at end of file diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index e3d2c3c..2ba5cb1 100755 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -4,13 +4,13 @@ + android:defaultValue="" + > + + + \ No newline at end of file diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index 97866ed..de1a4b9 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -4,6 +4,7 @@ */ package org.envaya.kalsms; +import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ContentValues; import android.content.Context; @@ -11,18 +12,28 @@ 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.util.Log; +import java.text.DateFormat; +import java.util.Date; +import org.apache.http.client.HttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; -/** - * - * @author Jesse - */ public class App { - public static final int OUTGOING_POLL_SECONDS = 30; - + public static final String ACTION_OUTGOING = "outgoing"; + public static final String ACTION_INCOMING = "incoming"; + public static final String ACTION_SEND_STATUS = "send_status"; + + public static final int STATUS_QUEUED = 1; + public static final int STATUS_FAILED = 2; + public static final int STATUS_SENT = 3; + 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"; @@ -30,17 +41,28 @@ public class App { public Context context; public SharedPreferences settings; - public App(Context context) + 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) + { + app = new App(context); + } + return app; + } + static void debug(String msg) { Log.d(LOG_NAME, msg); } - + public void log(String msg) { Log.d(LOG_NAME, msg); @@ -50,6 +72,34 @@ public void log(String msg) context.sendBroadcast(broadcast); } + public void setOutgoingMessageAlarm() + { + AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, + 0, + new Intent(context, OutgoingMessagePoller.class), + 0); + + alarm.cancel(pendingIntent); + + int pollSeconds = getOutgoingPollSeconds(); + + if (pollSeconds > 0) + { + alarm.setRepeating( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime(), + pollSeconds * 1000, + pendingIntent); + log("Checking for outgoing messages every " + pollSeconds + " sec"); + } + else + { + log("Not checking for outgoing messages."); + } + } + public void logError(Throwable ex) { logError("ERROR", ex); @@ -77,20 +127,17 @@ public void logError(String msg, Throwable ex, boolean detail) } } } - - public String getIncomingUrl() - { - return getServerUrl() + "/pg/receive_sms"; - } - - public String getOutgoingUrl() - { - return getServerUrl() + "/pg/dequeue_sms"; - } - - public String getSendStatusUrl() + + public String getDisplayString(String str) { - return getServerUrl() + "/pg/sms_sent"; + if (str.length() == 0) + { + return "(not set)"; + } + else + { + return str; + } } public String getServerUrl() @@ -103,21 +150,30 @@ public String getPhoneNumber() return settings.getString("phone_number", ""); } + public int getOutgoingPollSeconds() + { + return Integer.parseInt(settings.getString("outgoing_interval", "0")); + } + public String getPassword() { return settings.getString("password", ""); - } + } private SQLiteDatabase db; public SQLiteDatabase getWritableDatabase() { - if (db == null) - { - db = new DBHelper(context).getWritableDatabase(); - } - return db; + 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(); @@ -126,7 +182,7 @@ public void sendSMS(OutgoingSmsMessage sms) { SQLiteDatabase db = this.getWritableDatabase(); Cursor cursor = - db.rawQuery("select 1 from sent_sms where server_id=?", new String[] { serverId }); + db.rawQuery("select 1 from sms_status where server_id=?", new String[] { serverId }); boolean exists = (cursor.getCount() > 0); cursor.close(); @@ -138,7 +194,10 @@ public void sendSMS(OutgoingSmsMessage sms) ContentValues values = new ContentValues(); values.put("server_id", serverId); - db.insert("sent_sms", null, values); + values.put("status", App.STATUS_QUEUED); + db.insert("sms_status", null, values); + + db.close(); } SmsManager smgr = SmsManager.getDefault(); diff --git a/src/org/envaya/kalsms/DBHelper.java b/src/org/envaya/kalsms/DBHelper.java index 0710128..6271823 100755 --- a/src/org/envaya/kalsms/DBHelper.java +++ b/src/org/envaya/kalsms/DBHelper.java @@ -14,11 +14,14 @@ */ public class DBHelper extends SQLiteOpenHelper { - private static final int DATABASE_VERSION = 1; + private static final int DATABASE_VERSION = 2; private static final String DATABASE_NAME = "org.envaya.kalsms.db"; - private static final String SENT_SMS_TABLE_CREATE = - "CREATE TABLE sent_sms (server_id text);" + 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) { @@ -27,11 +30,14 @@ public DBHelper(Context context) { @Override public void onCreate(SQLiteDatabase db) { - db.execSQL(SENT_SMS_TABLE_CREATE); + 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 new file mode 100755 index 0000000..8eedc33 --- /dev/null +++ b/src/org/envaya/kalsms/HttpTask.java @@ -0,0 +1,181 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.envaya.kalsms; + +import android.os.AsyncTask; +import android.util.Base64; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +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.message.BasicNameValuePair; +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; + +public class HttpTask extends AsyncTask { + + private App app; + + public HttpTask(App app) + { + super(); + this.app = app; + } + + private String getSignature(String url, BasicNameValuePair... params) + { + try { + Arrays.sort(params, new Comparator() { + public int compare(Object o1, Object o2) + { + return ((BasicNameValuePair)o1).getName().compareTo(((BasicNameValuePair)o2).getName()); + } + }); + + StringBuilder builder = new StringBuilder(); + builder.append(url); + for (BasicNameValuePair param : params) + { + builder.append(","); + builder.append(param.getName()); + builder.append("="); + builder.append(param.getValue()); + } + builder.append(","); + builder.append(app.getPassword()); + + String value = builder.toString(); + + MessageDigest md = MessageDigest.getInstance("SHA-1"); + + md.update(value.getBytes("utf-8")); + + byte[] digest = md.digest(); + + return Base64.encodeToString(digest, Base64.NO_WRAP); + + } catch (Exception ex) { + app.logError("Error computing signature", ex); + } + return ""; + } + + protected HttpResponse doInBackground(BasicNameValuePair... params) { + try + { + String url = app.getServerUrl(); + HttpPost post = new HttpPost(url); + + post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params))); + + String signature = this.getSignature(url, params); + + post.setHeader("X-Kalsms-PhoneNumber", app.getPhoneNumber()); + post.setHeader("X-Kalsms-Signature", signature); + + HttpClient client = app.getHttpClient(); + HttpResponse response = client.execute(post); + + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200) + { + return response; + } + else if (statusCode == 403) + { + app.log("Failed to authenticate to server"); + app.log("(Phone number or password may be incorrect)"); + return null; + } + else + { + app.log("Received HTTP " + statusCode + " from server"); + return null; + } + } + catch (Throwable ex) + { + app.logError("Error while contacting server", ex); + return null; + } + } + + protected String getDefaultToAddress() + { + return ""; + } + + protected List parseResponseXML(HttpResponse response) + throws IOException, ParserConfigurationException, SAXException + { + List messages = new ArrayList(); + InputStream responseStream = response.getEntity().getContent(); + DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document xml = xmlBuilder.parse(responseStream); + + NodeList smsNodes = xml.getElementsByTagName("Sms"); + for (int i = 0; i < smsNodes.getLength(); i++) { + Element smsElement = (Element) smsNodes.item(i); + OutgoingSmsMessage sms = new OutgoingSmsMessage(); + + sms.setFrom(app.getPhoneNumber()); + + String to = smsElement.getAttribute("to"); + + sms.setTo(to.equals("") ? getDefaultToAddress() : to); + + String serverId = smsElement.getAttribute("id"); + + sms.setServerId(serverId.equals("") ? null : serverId); + + Node firstChild = smsElement.getFirstChild(); + sms.setMessage(firstChild != null ? firstChild.getNodeValue(): ""); + + messages.add(sms); + } + return messages; + } + + @Override + protected void onPostExecute(HttpResponse response) { + if (response != null) + { + try + { + handleResponse(response); + } + catch (Throwable ex) + { + app.logError("Error processing server response", ex); + handleFailure(); + } + } + else + { + handleFailure(); + } + } + + protected void handleResponse(HttpResponse response) throws Exception + { + } + + protected void handleFailure() + { + } +} diff --git a/src/org/envaya/kalsms/IncomingMessageForwarder.java b/src/org/envaya/kalsms/IncomingMessageForwarder.java index 3eb2727..bb336fb 100755 --- a/src/org/envaya/kalsms/IncomingMessageForwarder.java +++ b/src/org/envaya/kalsms/IncomingMessageForwarder.java @@ -1,121 +1,97 @@ package org.envaya.kalsms; -import android.app.Activity; -import android.app.PendingIntent; -import java.util.ArrayList; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.telephony.SmsManager; import android.telephony.SmsMessage; -import java.io.IOException; -import java.io.InputStream; +import java.util.ArrayList; import java.util.List; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import org.apache.http.HttpResponse; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -import org.apache.http.NameValuePair; -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; 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; + + } - public List sendMessageToServer(SmsMessage sms) { - - String message = sms.getDisplayMessageBody(); - String sender = sms.getDisplayOriginatingAddress(); - String recipient = app.getPhoneNumber(); + private class ForwarderTask extends HttpTask { - app.log("Received SMS from " + sender); + private SmsMessage originalSms; - if (message == null || message.length() == 0) { - return new ArrayList(); + public ForwarderTask(SmsMessage originalSms) { + super(app); + this.originalSms = originalSms; } - List replies = new ArrayList(); - - try { - - List params = new ArrayList(); - params.add(new BasicNameValuePair("from", sender)); - params.add(new BasicNameValuePair("to", recipient)); - params.add(new BasicNameValuePair("message", message)); - params.add(new BasicNameValuePair("secret", app.getPassword())); + @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(); + String recipient = app.getPhoneNumber(); - HttpClient client = new DefaultHttpClient(); - HttpPost post = new HttpPost(app.getIncomingUrl()); - post.setEntity(new UrlEncodedFormEntity(params)); + 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"); - HttpResponse response = client.execute(post); - - InputStream responseStream = response.getEntity().getContent(); - DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document xml = xmlBuilder.parse(responseStream); - - NodeList smsNodes = xml.getElementsByTagName("Sms"); - for (int i = 0; i < smsNodes.getLength(); i++) { - Element smsElement = (Element) smsNodes.item(i); - - OutgoingSmsMessage reply = new OutgoingSmsMessage(); - - reply.setFrom(recipient); - reply.setTo(sender); - reply.setMessage(smsElement.getFirstChild().getNodeValue()); - - replies.add(reply); - } - } catch (SAXException ex) { - app.logError("Error parsing response from server while forwarding incoming message", ex); - } catch (IOException ex) { - app.logError("Error forwarding incoming message to server", ex); - } catch (ParserConfigurationException ex) { - app.logError("Error configuring XML parser", ex); + new ForwarderTask(sms).execute( + new BasicNameValuePair("from", sender), + new BasicNameValuePair("to", recipient), + new BasicNameValuePair("message", message), + new BasicNameValuePair("action", App.ACTION_INCOMING) + ); } - - return replies; } public void smsReceived(Intent intent) { for (SmsMessage sms : getMessagesFromIntent(intent)) { - List replies = sendMessageToServer(sms); - - for (OutgoingSmsMessage reply : replies) - { - app.sendSMS(reply); - } + sendMessageToServer(sms); //DeleteSMSFromInbox(context, mesg); } } - @Override // source: http://www.devx.com/wireless/Article/39495/1954 public void onReceive(Context context, Intent intent) { try { - this.app = new App(context); + this.app = App.getInstance(context); String action = intent.getAction(); if (action.equals("android.provider.Telephony.SMS_RECEIVED")) { smsReceived(intent); - } + } } catch (Throwable ex) { app.logError("Unexpected error in IncomingMessageForwarder", ex, true); } @@ -123,30 +99,28 @@ public void onReceive(Context context, Intent intent) { /* private void DeleteSMSFromInbox(Context context, SmsMessage mesg) { - Log.d("KALSMS", "try to delete SMS"); - try { - Uri uriSms = Uri.parse("content://sms/inbox"); - StringBuilder sb = new StringBuilder(); - sb.append("address='" + mesg.getOriginatingAddress() + "' AND "); - sb.append("body='" + mesg.getMessageBody() + "'"); - Cursor c = context.getContentResolver().query(uriSms, null, sb.toString(), null, null); - c.moveToFirst(); - int thread_id = c.getInt(1); - context.getContentResolver().delete(Uri.parse("content://sms/conversations/" + thread_id), null, null); - c.close(); - } catch (Exception ex) { - // deletions don't work most of the time since the timing of the - // receipt and saving to the inbox - // makes it difficult to match up perfectly. the SMS might not be in - // the inbox yet when this receiver triggers! - Log.d("SmsReceiver", "Error deleting sms from inbox: " + ex.getMessage()); - } + Log.d("KALSMS", "try to delete SMS"); + try { + Uri uriSms = Uri.parse("content://sms/inbox"); + StringBuilder sb = new StringBuilder(); + sb.append("address='" + mesg.getOriginatingAddress() + "' AND "); + sb.append("body='" + mesg.getMessageBody() + "'"); + Cursor c = context.getContentResolver().query(uriSms, null, sb.toString(), null, null); + c.moveToFirst(); + int thread_id = c.getInt(1); + context.getContentResolver().delete(Uri.parse("content://sms/conversations/" + thread_id), null, null); + c.close(); + } catch (Exception ex) { + // deletions don't work most of the time since the timing of the + // receipt and saving to the inbox + // makes it difficult to match up perfectly. the SMS might not be in + // the inbox yet when this receiver triggers! + Log.d("SmsReceiver", "Error deleting sms from inbox: " + ex.getMessage()); } - */ - + } + */ // from http://github.com/dimagi/rapidandroid // source: http://www.devx.com/wireless/Article/39495/1954 - private SmsMessage[] getMessagesFromIntent(Intent intent) { SmsMessage retMsgs[] = null; Bundle bdl = intent.getExtras(); diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java index a4e4e8f..2ac991c 100755 --- a/src/org/envaya/kalsms/Main.java +++ b/src/org/envaya/kalsms/Main.java @@ -1,19 +1,20 @@ package org.envaya.kalsms; import android.app.Activity; -import android.app.AlarmManager; -import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; -import android.os.SystemClock; import android.preference.PreferenceManager; import android.text.Html; import android.text.method.ScrollingMovementMethod; import android.view.Menu; +import android.view.View; +import android.widget.ScrollView; import android.widget.TextView; +import java.text.DateFormat; +import java.util.Date; public class Main extends Activity { @@ -23,15 +24,54 @@ public void onReceive(Context context, Intent intent) { showLogMessage(intent.getExtras().getString("message")); } }; + + private long lastLogTime = 0; public void showLogMessage(String message) { TextView info = (TextView) Main.this.findViewById(R.id.info); if (message != null) { + int length = info.length(); + int maxLength = 20000; + if (length > maxLength) + { + CharSequence text = info.getText(); + + int startPos = length - maxLength / 2; + + for (int cur = startPos; cur < startPos + 100 && cur < length; cur++) + { + if (text.charAt(cur) == '\n') + { + startPos = cur; + break; + } + } + + CharSequence endSequence = text.subSequence(startPos, length); + + info.setText("[Older log messages not shown]"); + info.append(endSequence); + } + + long logTime = System.currentTimeMillis(); + if (logTime - lastLogTime > 60000) + { + Date date = new Date(logTime); + info.append("[" + DateFormat.getTimeInstance().format(date) + "]\n"); + lastLogTime = logTime; + } + info.append(message + "\n"); + + final ScrollView scrollView = (ScrollView) this.findViewById(R.id.info_scroll); + scrollView.post(new Runnable() { public void run() { + scrollView.fullScroll(View.FOCUS_DOWN); + } }); } } + public void onResume() { App.debug("RESUME"); @@ -45,40 +85,31 @@ public void onCreate(Bundle savedInstanceState) { App.debug("STARTED"); + App app = App.getInstance(this.getApplication()); + setContentView(R.layout.main); PreferenceManager.setDefaultValues(this, R.xml.prefs, false); - AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - - PendingIntent pendingIntent = PendingIntent.getBroadcast(this, - 0, - new Intent(this, OutgoingMessagePoller.class), - 0); - - alarm.setRepeating( - AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime(), - App.OUTGOING_POLL_SECONDS * 1000, - pendingIntent); - - App app = new App(this.getApplication()); - TextView info = (TextView) this.findViewById(R.id.info); info.setText(Html.fromHtml("SMS Gateway running.
")); + info.append(Html.fromHtml("Press Menu to edit settings.
")); - showLogMessage("Server URL is: " + app.getServerUrl()); - showLogMessage("Your phone number is: " + app.getPhoneNumber()); - showLogMessage("Checking for outgoing messages every " + App.OUTGOING_POLL_SECONDS + " sec"); - - info.append(Html.fromHtml("Press Menu to edit settings.
")); + showLogMessage("Server URL is: " + app.getDisplayString(app.getServerUrl())); + showLogMessage("Your phone number is: " + app.getDisplayString(app.getPhoneNumber())); info.setMovementMethod(new ScrollingMovementMethod()); - IntentFilter logReceiverFilter = new IntentFilter(); - + IntentFilter logReceiverFilter = new IntentFilter(); logReceiverFilter.addAction(App.LOG_INTENT); registerReceiver(logReceiver, logReceiverFilter); + + app.setOutgoingMessageAlarm(); + + for (int i = 0; i < 30; i++) + { + showLogMessage(" " + i); + } } // first time the Menu key is pressed diff --git a/src/org/envaya/kalsms/MessageStatusNotifier.java b/src/org/envaya/kalsms/MessageStatusNotifier.java index a2bad10..f059a80 100755 --- a/src/org/envaya/kalsms/MessageStatusNotifier.java +++ b/src/org/envaya/kalsms/MessageStatusNotifier.java @@ -9,73 +9,71 @@ import android.content.Context; import android.content.Intent; import android.telephony.SmsManager; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -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; public class MessageStatusNotifier extends BroadcastReceiver { private App app; - - public void notifySuccess(String serverId) + + public void notifyStatus(String serverId, int status, String errorMessage) { + String logMessage; + switch (status) + { + case App.STATUS_SENT: + logMessage = "sent successfully"; + break; + case App.STATUS_FAILED: + logMessage = "could not be sent (" + errorMessage + ")"; + break; + default: + logMessage = "queued"; + break; + } + String smsDesc = serverId == null ? "SMS reply" : ("SMS id=" + serverId); + if (serverId != null) { - try { - app.log("Notifying server of sent SMS id=" + serverId); - List params = new ArrayList(); - params.add(new BasicNameValuePair("from", app.getPhoneNumber())); - params.add(new BasicNameValuePair("secret", app.getPassword())); - params.add(new BasicNameValuePair("id", serverId)); - - HttpClient client = new DefaultHttpClient(); - HttpPost post = new HttpPost(app.getSendStatusUrl()); - post.setEntity(new UrlEncodedFormEntity(params)); + app.log("Notifying server " + smsDesc + " " + logMessage); - client.execute(post); - } - catch (IOException ex) - { - app.logError("Error while notifying server of outgoing message", ex); - } + new HttpTask(app).execute( + new BasicNameValuePair("from", app.getPhoneNumber()), + 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 = new App(context); + app = App.getInstance(context); String serverId = intent.getExtras().getString("serverId"); - String desc = serverId == null ? "SMS reply" : ("SMS id=" + serverId); - switch (getResultCode()) { case Activity.RESULT_OK: - app.log(desc + " sent successfully"); - this.notifySuccess(serverId); + this.notifyStatus(serverId, App.STATUS_SENT, ""); break; case SmsManager.RESULT_ERROR_GENERIC_FAILURE: - app.log(desc + " could not be sent (generic failure)"); + this.notifyStatus(serverId, App.STATUS_FAILED, "generic failure"); break; case SmsManager.RESULT_ERROR_RADIO_OFF: - app.log(desc + " could not be sent (radio off)"); + this.notifyStatus(serverId, App.STATUS_FAILED, "radio off"); break; case SmsManager.RESULT_ERROR_NO_SERVICE: - app.log(desc + " could not be sent (no service)"); + this.notifyStatus(serverId, App.STATUS_FAILED, "no service"); break; case SmsManager.RESULT_ERROR_NULL_PDU: - app.log(desc + " could not be sent (null PDU"); + this.notifyStatus(serverId, App.STATUS_FAILED, "null PDU"); break; default: - app.log("SMS could not be sent (unknown error)"); + this.notifyStatus(serverId, App.STATUS_FAILED, "unknown error"); break; } } diff --git a/src/org/envaya/kalsms/OutgoingMessagePoller.java b/src/org/envaya/kalsms/OutgoingMessagePoller.java index f204cac..5a4c085 100755 --- a/src/org/envaya/kalsms/OutgoingMessagePoller.java +++ b/src/org/envaya/kalsms/OutgoingMessagePoller.java @@ -1,89 +1,42 @@ package org.envaya.kalsms; -import java.util.ArrayList; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import org.apache.http.HttpResponse; -import org.apache.http.NameValuePair; -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.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; public class OutgoingMessagePoller extends BroadcastReceiver { - private App app; - - @Override - public void onReceive(Context context, Intent intent) { - try - { - app = new App(context); + private App app; - app.log("Checking for outgoing messages"); + private class PollerTask extends HttpTask { - for (OutgoingSmsMessage sms : getOutgoingMessages()) - { - app.sendSMS(sms); - } - } - catch (Throwable ex) - { - app.logError("Unexpected error in OutgoingMessagePoller", ex, true); - } - } + public PollerTask() + { + super(app); + } - public List getOutgoingMessages() { - List messages = new ArrayList(); - - try { - List params = new ArrayList(); - params.add(new BasicNameValuePair("from", app.getPhoneNumber())); - params.add(new BasicNameValuePair("secret", app.getPassword())); + @Override + protected void handleResponse(HttpResponse response) throws Exception { + for (OutgoingSmsMessage reply : parseResponseXML(response)) { + app.sendSMS(reply); + } + } + } - HttpClient client = new DefaultHttpClient(); - HttpPost post = new HttpPost(app.getOutgoingUrl()); - post.setEntity(new UrlEncodedFormEntity(params)); - - HttpResponse response = client.execute(post); + @Override + public void onReceive(Context context, Intent intent) { + app = App.getInstance(context); - InputStream responseStream = response.getEntity().getContent(); - DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - Document xml = xmlBuilder.parse(responseStream); - - NodeList smsNodes = xml.getElementsByTagName("Sms"); - for (int i = 0; i < smsNodes.getLength(); i++) { - Element smsElement = (Element) smsNodes.item(i); - OutgoingSmsMessage sms = new OutgoingSmsMessage(); - - sms.setFrom(app.getPhoneNumber()); - sms.setTo(smsElement.getAttribute("to")); - sms.setMessage(smsElement.getFirstChild().getNodeValue()); - sms.setServerId(smsElement.getAttribute("id")); - - messages.add(sms); - } - } catch (SAXException ex) { - app.logError("Error parsing response from server while retreiving outgoing messages", ex); - } catch (IOException ex) { - app.logError("Error retreiving outgoing messages from server", ex); - } catch (ParserConfigurationException ex) { - app.logError("Error configuring XML parser", ex); - } - - return messages; + String serverUrl = app.getServerUrl(); + if (serverUrl.length() > 0) + { + app.log("Checking for outgoing messages"); + new PollerTask().execute( + new BasicNameValuePair("from", app.getPhoneNumber()), + new BasicNameValuePair("action", App.ACTION_OUTGOING) + ); + } } - } diff --git a/src/org/envaya/kalsms/Prefs.java b/src/org/envaya/kalsms/Prefs.java index 4d00416..c254e9b 100755 --- a/src/org/envaya/kalsms/Prefs.java +++ b/src/org/envaya/kalsms/Prefs.java @@ -4,7 +4,11 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.os.Bundle; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; import android.view.Menu; public class Prefs extends PreferenceActivity implements OnSharedPreferenceChangeListener { @@ -13,47 +17,76 @@ public class Prefs extends PreferenceActivity implements OnSharedPreferenceChang public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.prefs); - } + + PreferenceScreen screen = this.getPreferenceScreen(); + int numPrefs = screen.getPreferenceCount(); + + for(int i=0; i < numPrefs;i++) + { + updatePrefSummary(screen.getPreference(i)); + } + } - @Override - protected void onResume() { + @Override + protected void onResume(){ super.onResume(); - // Set up a listener whenever a key changes + // Set up a listener whenever a key changes getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); } - @Override - protected void onPause() { + @Override + protected void onPause() { super.onPause(); - // Unregister the listener whenever a key changes - getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); - } + // Unregister the listener whenever a key changes + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + } - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - /* - Preference pref = findPreference(key); - if (pref instanceof EditTextPreference) { - EditTextPreference textPref = (EditTextPreference) pref; - pref.setSummary(textPref.getSummary()); - Log.d("KALSMS", "textPref.getSummary(): " + textPref.getSummary()); + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + + App app = App.getInstance(this.getApplication()); + if (key.equals("outgoing_interval")) + { + app.setOutgoingMessageAlarm(); } - if(pref instanceof CheckBoxPreference) { - CheckBoxPreference checkbox = (CheckBoxPreference) pref; - AlarmManager alarm = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - Intent pintent = new Intent(this, SMSSender.class); - PendingIntent pIntent = PendingIntent.getBroadcast(this,0,pintent, 0); - if(checkbox.isChecked()) { - alarm.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime(), - AlarmManager.INTERVAL_FIFTEEN_MINUTES, pIntent); - Log.d("KALSMS", "alarm manager turned on"); - } else { - alarm.cancel(pIntent); - Log.d("SMS_GATEWAY", "alarm manager turned off"); + else if (key.equals("server_url")) + { + app.log("Server URL changed to: " + app.getDisplayString(app.getServerUrl())); } + else if (key.equals("phone_number")) + { + app.log("Phone number changed to: " + app.getDisplayString(app.getPhoneNumber())); } - */ - } + else if (key.equals("password")) + { + app.log("Password changed"); + } + + updatePrefSummary(findPreference(key)); + } + + private void updatePrefSummary(Preference p) + { + if (p instanceof ListPreference) { + p.setSummary(((ListPreference)p).getEntry()); + } + else if (p instanceof EditTextPreference) { + + EditTextPreference textPref = (EditTextPreference)p; + String text = textPref.getText(); + if (text.equals("")) + { + p.setSummary("(not set)"); + } + else if (p.getKey().equals("password")) + { + p.setSummary("********"); + } + else + { + p.setSummary(text); + } + } + } // first time the Menu key is pressed @Override From 8f15a8c8f2765278272fbf2b5d4bb06ac4eb15f9 Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Sun, 11 Sep 2011 21:21:42 -0700 Subject: [PATCH 10/69] add menu with options to test server connection, or edit settings --- res/menu/mainmenu.xml | 9 ++++++ res/values/strings.xml | 2 ++ src/org/envaya/kalsms/App.java | 1 + src/org/envaya/kalsms/Main.java | 56 +++++++++++++++++++++++++-------- 4 files changed, 55 insertions(+), 13 deletions(-) create mode 100755 res/menu/mainmenu.xml diff --git a/res/menu/mainmenu.xml b/res/menu/mainmenu.xml new file mode 100755 index 0000000..4c3ea24 --- /dev/null +++ b/res/menu/mainmenu.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index b48bf11..12257ae 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,4 +1,6 @@ KalSMS Envaya + Settings + Test diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index de1a4b9..d5bd02f 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -29,6 +29,7 @@ public class App { public static final String ACTION_OUTGOING = "outgoing"; public static final String ACTION_INCOMING = "incoming"; public static final String ACTION_SEND_STATUS = "send_status"; + public static final String ACTION_TEST = "test"; public static final int STATUS_QUEUED = 1; public static final int STATUS_FAILED = 2; diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java index 2ac991c..b312299 100755 --- a/src/org/envaya/kalsms/Main.java +++ b/src/org/envaya/kalsms/Main.java @@ -10,14 +10,20 @@ import android.text.Html; import android.text.method.ScrollingMovementMethod; import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.widget.ScrollView; import android.widget.TextView; import java.text.DateFormat; import java.util.Date; +import org.apache.http.HttpResponse; +import org.apache.http.message.BasicNameValuePair; public class Main extends Activity { + private App app; + private BroadcastReceiver logReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -25,6 +31,19 @@ public void onReceive(Context context, Intent intent) { } }; + private class TestTask extends HttpTask + { + public TestTask() { + super(app); + } + + @Override + protected void handleResponse(HttpResponse response) throws Exception + { + app.log("Server connection OK!"); + } + } + private long lastLogTime = 0; public void showLogMessage(String message) @@ -85,7 +104,7 @@ public void onCreate(Bundle savedInstanceState) { App.debug("STARTED"); - App app = App.getInstance(this.getApplication()); + this.app = App.getInstance(this.getApplication()); setContentView(R.layout.main); PreferenceManager.setDefaultValues(this, R.xml.prefs, false); @@ -104,25 +123,36 @@ public void onCreate(Bundle savedInstanceState) { logReceiverFilter.addAction(App.LOG_INTENT); registerReceiver(logReceiver, logReceiverFilter); - app.setOutgoingMessageAlarm(); - - for (int i = 0; i < 30; i++) - { - showLogMessage(" " + i); - } + app.setOutgoingMessageAlarm(); } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.settings: + startActivity(new Intent(this, Prefs.class)); + return true; + case R.id.test: + app.log("Testing server connection..."); + new TestTask().execute( + new BasicNameValuePair("action", App.ACTION_TEST) + ); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + // first time the Menu key is pressed + @Override public boolean onCreateOptionsMenu(Menu menu) { - startActivity(new Intent(this, Prefs.class)); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.mainmenu, menu); + return(true); } - // any other time the Menu key is pressed - public boolean onPrepareOptionsMenu(Menu menu) { - startActivity(new Intent(this, Prefs.class)); - return(true); - } @Override protected void onStop(){ From d5b973411b30ffe6d438c2d38816a34c4c2b5e4b Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Mon, 12 Sep 2011 12:55:33 -0700 Subject: [PATCH 11/69] fix null pointer exception; remove dependency on Base64 for android 1.6 compatibility --- default.properties | 2 +- res/values/strings.xml | 2 +- src/org/envaya/kalsms/App.java | 3 - src/org/envaya/kalsms/Base64Coder.java | 269 +++++++++++++++++++++++++ src/org/envaya/kalsms/HttpTask.java | 3 +- src/org/envaya/kalsms/Prefs.java | 2 +- 6 files changed, 273 insertions(+), 8 deletions(-) create mode 100755 src/org/envaya/kalsms/Base64Coder.java diff --git a/default.properties b/default.properties index 4735723..9d6f70d 100644 --- a/default.properties +++ b/default.properties @@ -8,6 +8,6 @@ # project structure. # Project target. -target=android-8 +target=android-4 # Indicates whether an apk should be generated for each density. split.density=false diff --git a/res/values/strings.xml b/res/values/strings.xml index 12257ae..b3b6094 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,6 +1,6 @@ - KalSMS Envaya + KalSMS 2 Settings Test diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index d5bd02f..3d32f0f 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -16,8 +16,6 @@ import android.preference.PreferenceManager; import android.telephony.SmsManager; import android.util.Log; -import java.text.DateFormat; -import java.util.Date; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.BasicHttpParams; @@ -161,7 +159,6 @@ public String getPassword() return settings.getString("password", ""); } - private SQLiteDatabase db; public SQLiteDatabase getWritableDatabase() { return new DBHelper(context).getWritableDatabase(); diff --git a/src/org/envaya/kalsms/Base64Coder.java b/src/org/envaya/kalsms/Base64Coder.java new file mode 100755 index 0000000..2652cea --- /dev/null +++ b/src/org/envaya/kalsms/Base64Coder.java @@ -0,0 +1,269 @@ +// Copyright 2003-2010 Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland +// www.source-code.biz, www.inventec.ch/chdh +// +// This module is multi-licensed and may be used under the terms +// of any of the following licenses: +// +// EPL, Eclipse Public License, V1.0 or later, http://www.eclipse.org/legal +// LGPL, GNU Lesser General Public License, V2.1 or later, http://www.gnu.org/licenses/lgpl.html +// GPL, GNU General Public License, V2 or later, http://www.gnu.org/licenses/gpl.html +// AL, Apache License, V2.0 or later, http://www.apache.org/licenses +// BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php +// MIT, MIT License, http://www.opensource.org/licenses/MIT +// +// Please contact the author if you need another license. +// This module is provided "as is", without warranties of any kind. +package org.envaya.kalsms; + +/** + * A Base64 encoder/decoder. + * + *

+ * This class is used to encode and decode data in Base64 format as described in RFC 1521. + * + *

+ * Project home page: www.source-code.biz/base64coder/java
+ * Author: Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland
+ * Multi-licensed: EPL / LGPL / GPL / AL / BSD / MIT. + */ +public class Base64Coder { + +// The line separator string of the operating system. + private static final String systemLineSeparator = System.getProperty("line.separator"); +// Mapping table from 6-bit nibbles to Base64 characters. + private static final char[] map1 = new char[64]; + + static { + int i = 0; + for (char c = 'A'; c <= 'Z'; c++) { + map1[i++] = c; + } + for (char c = 'a'; c <= 'z'; c++) { + map1[i++] = c; + } + for (char c = '0'; c <= '9'; c++) { + map1[i++] = c; + } + map1[i++] = '+'; + map1[i++] = '/'; + } +// Mapping table from Base64 characters to 6-bit nibbles. + private static final byte[] map2 = new byte[128]; + + static { + for (int i = 0; i < map2.length; i++) { + map2[i] = -1; + } + for (int i = 0; i < 64; i++) { + map2[map1[i]] = (byte) i; + } + } + + /** + * Encodes a string into Base64 format. + * No blanks or line breaks are inserted. + * @param s A String to be encoded. + * @return A String containing the Base64 encoded data. + */ + public static String encodeString(String s) { + return new String(encode(s.getBytes())); + } + + /** + * Encodes a byte array into Base 64 format and breaks the output into lines of 76 characters. + * This method is compatible with sun.misc.BASE64Encoder.encodeBuffer(byte[]). + * @param in An array containing the data bytes to be encoded. + * @return A String containing the Base64 encoded data, broken into lines. + */ + public static String encodeLines(byte[] in) { + return encodeLines(in, 0, in.length, 76, systemLineSeparator); + } + + /** + * Encodes a byte array into Base 64 format and breaks the output into lines. + * @param in An array containing the data bytes to be encoded. + * @param iOff Offset of the first byte in in to be processed. + * @param iLen Number of bytes to be processed in in, starting at iOff. + * @param lineLen Line length for the output data. Should be a multiple of 4. + * @param lineSeparator The line separator to be used to separate the output lines. + * @return A String containing the Base64 encoded data, broken into lines. + */ + public static String encodeLines(byte[] in, int iOff, int iLen, int lineLen, String lineSeparator) { + int blockLen = (lineLen * 3) / 4; + if (blockLen <= 0) { + throw new IllegalArgumentException(); + } + int lines = (iLen + blockLen - 1) / blockLen; + int bufLen = ((iLen + 2) / 3) * 4 + lines * lineSeparator.length(); + StringBuilder buf = new StringBuilder(bufLen); + int ip = 0; + while (ip < iLen) { + int l = Math.min(iLen - ip, blockLen); + buf.append(encode(in, iOff + ip, l)); + buf.append(lineSeparator); + ip += l; + } + return buf.toString(); + } + + /** + * Encodes a byte array into Base64 format. + * No blanks or line breaks are inserted in the output. + * @param in An array containing the data bytes to be encoded. + * @return A character array containing the Base64 encoded data. + */ + public static char[] encode(byte[] in) { + return encode(in, 0, in.length); + } + + /** + * Encodes a byte array into Base64 format. + * No blanks or line breaks are inserted in the output. + * @param in An array containing the data bytes to be encoded. + * @param iLen Number of bytes to process in in. + * @return A character array containing the Base64 encoded data. + */ + public static char[] encode(byte[] in, int iLen) { + return encode(in, 0, iLen); + } + + /** + * Encodes a byte array into Base64 format. + * No blanks or line breaks are inserted in the output. + * @param in An array containing the data bytes to be encoded. + * @param iOff Offset of the first byte in in to be processed. + * @param iLen Number of bytes to process in in, starting at iOff. + * @return A character array containing the Base64 encoded data. + */ + public static char[] encode(byte[] in, int iOff, int iLen) { + int oDataLen = (iLen * 4 + 2) / 3; // output length without padding + int oLen = ((iLen + 2) / 3) * 4; // output length including padding + char[] out = new char[oLen]; + int ip = iOff; + int iEnd = iOff + iLen; + int op = 0; + while (ip < iEnd) { + int i0 = in[ip++] & 0xff; + int i1 = ip < iEnd ? in[ip++] & 0xff : 0; + int i2 = ip < iEnd ? in[ip++] & 0xff : 0; + int o0 = i0 >>> 2; + int o1 = ((i0 & 3) << 4) | (i1 >>> 4); + int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); + int o3 = i2 & 0x3F; + out[op++] = map1[o0]; + out[op++] = map1[o1]; + out[op] = op < oDataLen ? map1[o2] : '='; + op++; + out[op] = op < oDataLen ? map1[o3] : '='; + op++; + } + return out; + } + + /** + * Decodes a string from Base64 format. + * No blanks or line breaks are allowed within the Base64 encoded input data. + * @param s A Base64 String to be decoded. + * @return A String containing the decoded data. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static String decodeString(String s) { + return new String(decode(s)); + } + + /** + * Decodes a byte array from Base64 format and ignores line separators, tabs and blanks. + * CR, LF, Tab and Space characters are ignored in the input data. + * This method is compatible with sun.misc.BASE64Decoder.decodeBuffer(String). + * @param s A Base64 String to be decoded. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decodeLines(String s) { + char[] buf = new char[s.length()]; + int p = 0; + for (int ip = 0; ip < s.length(); ip++) { + char c = s.charAt(ip); + if (c != ' ' && c != '\r' && c != '\n' && c != '\t') { + buf[p++] = c; + } + } + return decode(buf, 0, p); + } + + /** + * Decodes a byte array from Base64 format. + * No blanks or line breaks are allowed within the Base64 encoded input data. + * @param s A Base64 String to be decoded. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decode(String s) { + return decode(s.toCharArray()); + } + + /** + * Decodes a byte array from Base64 format. + * No blanks or line breaks are allowed within the Base64 encoded input data. + * @param in A character array containing the Base64 encoded data. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decode(char[] in) { + return decode(in, 0, in.length); + } + + /** + * Decodes a byte array from Base64 format. + * No blanks or line breaks are allowed within the Base64 encoded input data. + * @param in A character array containing the Base64 encoded data. + * @param iOff Offset of the first character in in to be processed. + * @param iLen Number of characters to process in in, starting at iOff. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decode(char[] in, int iOff, int iLen) { + if (iLen % 4 != 0) { + throw new IllegalArgumentException("Length of Base64 encoded input string is not a multiple of 4."); + } + while (iLen > 0 && in[iOff + iLen - 1] == '=') { + iLen--; + } + int oLen = (iLen * 3) / 4; + byte[] out = new byte[oLen]; + int ip = iOff; + int iEnd = iOff + iLen; + int op = 0; + while (ip < iEnd) { + int i0 = in[ip++]; + int i1 = in[ip++]; + int i2 = ip < iEnd ? in[ip++] : 'A'; + int i3 = ip < iEnd ? in[ip++] : 'A'; + if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) { + throw new IllegalArgumentException("Illegal character in Base64 encoded data."); + } + int b0 = map2[i0]; + int b1 = map2[i1]; + int b2 = map2[i2]; + int b3 = map2[i3]; + if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) { + throw new IllegalArgumentException("Illegal character in Base64 encoded data."); + } + int o0 = (b0 << 2) | (b1 >>> 4); + int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); + int o2 = ((b2 & 3) << 6) | b3; + out[op++] = (byte) o0; + if (op < oLen) { + out[op++] = (byte) o1; + } + if (op < oLen) { + out[op++] = (byte) o2; + } + } + return out; + } + +// Dummy constructor. + private Base64Coder() { + } +} // end class Base64Coder diff --git a/src/org/envaya/kalsms/HttpTask.java b/src/org/envaya/kalsms/HttpTask.java index 8eedc33..c9737dd 100755 --- a/src/org/envaya/kalsms/HttpTask.java +++ b/src/org/envaya/kalsms/HttpTask.java @@ -5,7 +5,6 @@ package org.envaya.kalsms; import android.os.AsyncTask; -import android.util.Base64; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; @@ -67,7 +66,7 @@ public int compare(Object o1, Object o2) byte[] digest = md.digest(); - return Base64.encodeToString(digest, Base64.NO_WRAP); + return new String(Base64Coder.encode(digest)); } catch (Exception ex) { app.logError("Error computing signature", ex); diff --git a/src/org/envaya/kalsms/Prefs.java b/src/org/envaya/kalsms/Prefs.java index c254e9b..d09ee61 100755 --- a/src/org/envaya/kalsms/Prefs.java +++ b/src/org/envaya/kalsms/Prefs.java @@ -73,7 +73,7 @@ else if (p instanceof EditTextPreference) { EditTextPreference textPref = (EditTextPreference)p; String text = textPref.getText(); - if (text.equals("")) + if (text == null || text.equals("")) { p.setSummary("(not set)"); } From cb8d95ebd58a4cf31e38882946e45b5e8cca2040 Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Mon, 12 Sep 2011 16:53:38 -0700 Subject: [PATCH 12/69] PHP server library with example implementation; add 'check now' button on menu --- .gitmodules | 3 + res/menu/mainmenu.xml | 3 + res/values/strings.xml | 1 + server/php/KalSMS.php | 177 ++++++++++++++++++ server/php/README.txt | 25 +++ server/php/example/config.php | 9 + server/php/example/httpserver | 1 + server/php/example/send_sms.php | 41 ++++ server/php/example/server.php | 46 +++++ server/php/example/test_sms.php | 6 + server/php/example/www/index.php | 91 +++++++++ src/org/envaya/kalsms/App.java | 34 ++++ src/org/envaya/kalsms/HttpTask.java | 1 + .../kalsms/IncomingMessageForwarder.java | 2 - src/org/envaya/kalsms/Main.java | 3 + .../envaya/kalsms/OutgoingMessagePoller.java | 28 +-- 16 files changed, 442 insertions(+), 29 deletions(-) create mode 100644 .gitmodules create mode 100755 server/php/KalSMS.php create mode 100755 server/php/README.txt create mode 100755 server/php/example/config.php create mode 160000 server/php/example/httpserver create mode 100755 server/php/example/send_sms.php create mode 100755 server/php/example/server.php create mode 100755 server/php/example/test_sms.php create mode 100755 server/php/example/www/index.php diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..589e214 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "server/php/example/httpserver"] + path = server/php/example/httpserver + url = git://github.com/youngj/httpserver.git diff --git a/res/menu/mainmenu.xml b/res/menu/mainmenu.xml index 4c3ea24..cc7148d 100755 --- a/res/menu/mainmenu.xml +++ b/res/menu/mainmenu.xml @@ -6,4 +6,7 @@ + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index b3b6094..8dc1820 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3,4 +3,5 @@ KalSMS 2 Settings Test + Check Now diff --git a/server/php/KalSMS.php b/server/php/KalSMS.php new file mode 100755 index 0000000..3c9a39d --- /dev/null +++ b/server/php/KalSMS.php @@ -0,0 +1,177 @@ +compute_signature($full_url, $_POST, $correct_password); + + //error_log("Correct signature: '$correct_signature'"); + + return $signature === $correct_signature; + } + + function compute_signature($url, $data, $password) + { + ksort($data); + + $input = $url; + foreach($data as $key => $value) + $input .= ",$key=$value"; + + $input .= ",$password"; + + //error_log("Signed data: '$input'"); + + return base64_encode(sha1($input, true)); + } +} + +class KalSMS_OutgoingMessage +{ + public $id = ''; + public $to; + public $message; +} + +class KalSMS_Action +{ + public $type; + public $kalsms; + + function __construct($kalsms) + { + $this->kalsms = $kalsms; + } +} + +class KalSMS_Action_Test extends KalSMS_Action +{ + function __construct($kalsms) + { + parent::__construct($kalsms); + $this->type = KalSMS::ACTION_TEST; + } +} + +class KalSMS_Action_Incoming extends KalSMS_Action +{ + public $from; + public $message; + + function __construct($kalsms) + { + parent::__construct($kalsms); + $this->type = KalSMS::ACTION_INCOMING; + $this->from = $_POST['from']; + $this->message = $_POST['message']; + } + + function get_response_xml($messages) + { + ob_start(); + echo "\n"; + echo ""; + foreach ($messages as $message) + { + echo "".KalSMS::escape($message->message).""; + } + echo ""; + return ob_get_clean(); + } +} + +class KalSMS_Action_Outgoing extends KalSMS_Action +{ + function __construct($kalsms) + { + parent::__construct($kalsms); + $this->type = KalSMS::ACTION_OUTGOING; + } + + function get_response_xml($messages) + { + ob_start(); + echo "\n"; + echo ""; + foreach ($messages as $message) + { + echo "". + KalSMS::escape($message->message).""; + } + echo ""; + return ob_get_clean(); + } +} + +class KalSMS_Action_SendStatus extends KalSMS_Action +{ + public $status; + public $id; + + function __construct($type) + { + $this->type = KalSMS::ACTION_SEND_STATUS; + $this->status = (int)$_POST['status']; + $this->id = $_POST['id']; + } +} \ No newline at end of file diff --git a/server/php/README.txt b/server/php/README.txt new file mode 100755 index 0000000..cabf434 --- /dev/null +++ b/server/php/README.txt @@ -0,0 +1,25 @@ + +PHP server library for KalSMS, with example implementation + +The KalSMS.php library is intended to be used as part of a PHP application +running on an HTTP server that receives incoming SMS messages from, and sends +outgoing SMS messages to an Android phone running KalSMS. + +To run the example implementation, the example/www/ directory should be made available +via a web server running PHP (e.g. Apache). You can also use the included standalone +PHP web server, by running the following commands: + git submodule init + php server.php + +example/config.php contains the list of phone numbers and passwords for phones running KalSMS. + +On a phone running KalSMS, go to Menu -> Settings and enter: + * Server URL: The URL to example/www/index.php. + If you're using server.php, this will be http://:8002/ + * Your phone number: One of the phone numbers listed in example/config.php + * Password: The corresponding password in example/config.php + +To send an outgoing SMS, use + php example/send_sms.php + +See KalSMS.php and example/www/index.php \ No newline at end of file diff --git a/server/php/example/config.php b/server/php/example/config.php new file mode 100755 index 0000000..6a6cadf --- /dev/null +++ b/server/php/example/config.php @@ -0,0 +1,9 @@ + 'rosebud', + '16505551213' => 's3krit', +); +$PHONE_NUMBERS = array_keys($PASSWORDS); + +$OUTGOING_DIR_NAME = __DIR__."/outgoing_sms"; \ No newline at end of file diff --git a/server/php/example/httpserver b/server/php/example/httpserver new file mode 160000 index 0000000..95a38ab --- /dev/null +++ b/server/php/example/httpserver @@ -0,0 +1 @@ +Subproject commit 95a38ab08f14ac54dc64b6f6c008d7f69af8a8f4 diff --git a/server/php/example/send_sms.php b/server/php/example/send_sms.php new file mode 100755 index 0000000..821f638 --- /dev/null +++ b/server/php/example/send_sms.php @@ -0,0 +1,41 @@ +] \"\""); + error_log("Examples: "); + error_log(" php send_sms.php 16505551212 16504449876 \"hello world\""); + error_log(" php send_sms.php 16504449876 \"hello world\""); + die; +} + +$id = uniqid(""); + +$filename = "$OUTGOING_DIR_NAME/$id.json"; + +file_put_contents($filename, json_encode(array( + 'from' => $from, + 'to' => $to, + 'message' => $message, + 'id' => $id +))); + +echo "Message $id added to outgoing queue\n"; \ No newline at end of file diff --git a/server/php/example/server.php b/server/php/example/server.php new file mode 100755 index 0000000..1eab65f --- /dev/null +++ b/server/php/example/server.php @@ -0,0 +1,46 @@ + 8002, + )); + } + + function route_request($request) + { + $uri = $request->uri; + + $doc_root = __DIR__ . '/www'; + + if (preg_match('#/$#', $uri)) + { + $uri .= "index.php"; + } + + if (preg_match('#\.php$#', $uri)) + { + return $this->get_php_response($request, "$doc_root$uri"); + } + else + { + return $this->get_static_response($request, "$doc_root$uri"); + } + } +} + +$server = new ExampleServer(); +$server->run_forever(); \ No newline at end of file diff --git a/server/php/example/test_sms.php b/server/php/example/test_sms.php new file mode 100755 index 0000000..d56d634 --- /dev/null +++ b/server/php/example/test_sms.php @@ -0,0 +1,6 @@ +get_request_phone_number(); + +$password = @$PASSWORDS[$phone_number]; + +if (!isset($password) || !$kalsms->is_validated_request($password)) +{ + header("HTTP/1.1 403 Forbidden"); + error_log("Invalid request signature"); + echo "Invalid request signature"; + return; +} + +$action = $kalsms->get_request_action(); + +switch ($action->type) +{ + case KalSMS::ACTION_INCOMING: + error_log("Received SMS from {$action->from}"); + + $reply = new KalSMS_OutgoingMessage(); + $reply->message = "You said: {$action->message}"; + + error_log("Sending reply: {$reply->message}"); + + header("Content-Type: text/xml"); + echo $action->get_response_xml(array($reply)); + return; + + case KalSMS::ACTION_OUTGOING: + $messages = array(); + + $dir = opendir($OUTGOING_DIR_NAME); + while ($file = readdir($dir)) + { + if (preg_match('#\.json$#', $file)) + { + $data = json_decode(file_get_contents("$OUTGOING_DIR_NAME/$file"), true); + if ($data && @$data['from'] == $phone_number) + { + $sms = new KalSMS_OutgoingMessage(); + $sms->id = $data['id']; + $sms->to = $data['to']; + $sms->from = $data['from']; + $sms->message = $data['message']; + $messages[] = $sms; + } + } + } + closedir($dir); + + header("Content-Type: text/xml"); + echo $action->get_response_xml($messages); + return; + + case KalSMS::ACTION_SEND_STATUS: + + $id = $action->id; + + // delete file with matching id + if (preg_match('#^\w+$#', $id) && unlink("$OUTGOING_DIR_NAME/$id.json")) + { + echo "OK"; + } + else + { + header("HTTP/1.1 404 Not Found"); + echo "invalid id"; + } + return; + + case KalSMS::ACTION_TEST: + echo "OK"; + return; + + default: + header("HTTP/1.1 404 Not Found"); + echo "Invalid action"; + return; +} \ No newline at end of file diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index 3d32f0f..7300214 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -16,8 +16,10 @@ import android.preference.PreferenceManager; import android.telephony.SmsManager; import android.util.Log; +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; @@ -71,6 +73,22 @@ public void log(String msg) context.sendBroadcast(broadcast); } + public void checkOutgoingMessages() + { + String serverUrl = getServerUrl(); + if (serverUrl.length() > 0) + { + log("Checking for outgoing messages"); + new PollerTask().execute( + new BasicNameValuePair("action", App.ACTION_OUTGOING) + ); + } + else + { + log("Can't check outgoing messages; server URL not set"); + } + } + public void setOutgoingMessageAlarm() { AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); @@ -212,4 +230,20 @@ public void sendSMS(OutgoingSmsMessage sms) log("Sending " +sms.getLogName() + " to " + sms.getTo()); smgr.sendTextMessage(sms.getTo(), null, sms.getMessage(), sentIntent, null); } + + private class PollerTask extends HttpTask { + + public PollerTask() + { + super(app); + } + + @Override + protected void handleResponse(HttpResponse response) throws Exception { + for (OutgoingSmsMessage reply : parseResponseXML(response)) { + app.sendSMS(reply); + } + } + } + } diff --git a/src/org/envaya/kalsms/HttpTask.java b/src/org/envaya/kalsms/HttpTask.java index c9737dd..74fd4db 100755 --- a/src/org/envaya/kalsms/HttpTask.java +++ b/src/org/envaya/kalsms/HttpTask.java @@ -84,6 +84,7 @@ protected HttpResponse doInBackground(BasicNameValuePair... params) { String signature = this.getSignature(url, params); + post.setHeader("X-Kalsms-Version", "2"); post.setHeader("X-Kalsms-PhoneNumber", app.getPhoneNumber()); post.setHeader("X-Kalsms-Signature", signature); diff --git a/src/org/envaya/kalsms/IncomingMessageForwarder.java b/src/org/envaya/kalsms/IncomingMessageForwarder.java index bb336fb..27e9120 100755 --- a/src/org/envaya/kalsms/IncomingMessageForwarder.java +++ b/src/org/envaya/kalsms/IncomingMessageForwarder.java @@ -53,7 +53,6 @@ public void sendMessageToServer(SmsMessage sms) String serverUrl = app.getServerUrl(); String message = sms.getMessageBody(); String sender = sms.getOriginatingAddress(); - String recipient = app.getPhoneNumber(); app.log("Received SMS from " + sender); @@ -64,7 +63,6 @@ public void sendMessageToServer(SmsMessage sms) new ForwarderTask(sms).execute( new BasicNameValuePair("from", sender), - new BasicNameValuePair("to", recipient), new BasicNameValuePair("message", message), new BasicNameValuePair("action", App.ACTION_INCOMING) ); diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java index b312299..686a7e5 100755 --- a/src/org/envaya/kalsms/Main.java +++ b/src/org/envaya/kalsms/Main.java @@ -133,6 +133,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.settings: startActivity(new Intent(this, Prefs.class)); return true; + case R.id.check_now: + app.checkOutgoingMessages(); + return true; case R.id.test: app.log("Testing server connection..."); new TestTask().execute( diff --git a/src/org/envaya/kalsms/OutgoingMessagePoller.java b/src/org/envaya/kalsms/OutgoingMessagePoller.java index 5a4c085..a8c9018 100755 --- a/src/org/envaya/kalsms/OutgoingMessagePoller.java +++ b/src/org/envaya/kalsms/OutgoingMessagePoller.java @@ -3,40 +3,14 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import org.apache.http.HttpResponse; -import org.apache.http.message.BasicNameValuePair; public class OutgoingMessagePoller extends BroadcastReceiver { private App app; - private class PollerTask extends HttpTask { - - public PollerTask() - { - super(app); - } - - @Override - protected void handleResponse(HttpResponse response) throws Exception { - for (OutgoingSmsMessage reply : parseResponseXML(response)) { - app.sendSMS(reply); - } - } - } - @Override public void onReceive(Context context, Intent intent) { app = App.getInstance(context); - - String serverUrl = app.getServerUrl(); - if (serverUrl.length() > 0) - { - app.log("Checking for outgoing messages"); - new PollerTask().execute( - new BasicNameValuePair("from", app.getPhoneNumber()), - new BasicNameValuePair("action", App.ACTION_OUTGOING) - ); - } + app.checkOutgoingMessages(); } } From aff22e51d82953d8b9ef131aa8bbad29c8a02d3b Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Mon, 12 Sep 2011 17:37:37 -0700 Subject: [PATCH 13/69] use string status codes instead of integers --- server/php/KalSMS.php | 19 +++++++++++--- server/php/example/outgoing_sms/.gitignore | 1 + src/org/envaya/kalsms/App.java | 6 ++--- .../envaya/kalsms/MessageStatusNotifier.java | 25 +++++++++---------- 4 files changed, 31 insertions(+), 20 deletions(-) create mode 100644 server/php/example/outgoing_sms/.gitignore diff --git a/server/php/KalSMS.php b/server/php/KalSMS.php index 3c9a39d..bc11040 100755 --- a/server/php/KalSMS.php +++ b/server/php/KalSMS.php @@ -13,9 +13,9 @@ class KalSMS const ACTION_SEND_STATUS = 'send_status'; const ACTION_TEST = 'test'; - const STATUS_QUEUED = 1; - const STATUS_FAILED = 2; - const STATUS_SENT = 3; + const STATUS_QUEUED = 'queued'; + const STATUS_FAILED = 'failed'; + const STATUS_SENT = 'sent'; static function new_from_request() { @@ -29,7 +29,18 @@ static function escape($val) return htmlspecialchars($val, ENT_QUOTES, 'UTF-8'); } + private $request_action; + function get_request_action() + { + if (!$this->request_action) + { + $this->request_action = $this->_get_request_action(); + } + return $this->request_action; + } + + private function _get_request_action() { switch (@$_POST['action']) { @@ -171,7 +182,7 @@ class KalSMS_Action_SendStatus extends KalSMS_Action function __construct($type) { $this->type = KalSMS::ACTION_SEND_STATUS; - $this->status = (int)$_POST['status']; + $this->status = $_POST['status']; $this->id = $_POST['id']; } } \ No newline at end of file diff --git a/server/php/example/outgoing_sms/.gitignore b/server/php/example/outgoing_sms/.gitignore new file mode 100644 index 0000000..94a2dd1 --- /dev/null +++ b/server/php/example/outgoing_sms/.gitignore @@ -0,0 +1 @@ +*.json \ No newline at end of file diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index 7300214..3388ee5 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -31,9 +31,9 @@ public class App { public static final String ACTION_SEND_STATUS = "send_status"; public static final String ACTION_TEST = "test"; - public static final int STATUS_QUEUED = 1; - public static final int STATUS_FAILED = 2; - public static final int STATUS_SENT = 3; + public static final String STATUS_QUEUED = "queued"; + 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_INTENT = "org.envaya.kalsms.LOG"; diff --git a/src/org/envaya/kalsms/MessageStatusNotifier.java b/src/org/envaya/kalsms/MessageStatusNotifier.java index f059a80..b4b6da0 100755 --- a/src/org/envaya/kalsms/MessageStatusNotifier.java +++ b/src/org/envaya/kalsms/MessageStatusNotifier.java @@ -15,20 +15,20 @@ public class MessageStatusNotifier extends BroadcastReceiver { private App app; - public void notifyStatus(String serverId, int status, String errorMessage) + public void notifyStatus(String serverId, String status, String errorMessage) { String logMessage; - switch (status) + if (status.equals(App.STATUS_SENT)) { - case App.STATUS_SENT: - logMessage = "sent successfully"; - break; - case App.STATUS_FAILED: - logMessage = "could not be sent (" + errorMessage + ")"; - break; - default: - logMessage = "queued"; - break; + 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); @@ -37,9 +37,8 @@ public void notifyStatus(String serverId, int status, String errorMessage) app.log("Notifying server " + smsDesc + " " + logMessage); new HttpTask(app).execute( - new BasicNameValuePair("from", app.getPhoneNumber()), new BasicNameValuePair("id", serverId), - new BasicNameValuePair("status", "" + status), + new BasicNameValuePair("status", status), new BasicNameValuePair("error", errorMessage), new BasicNameValuePair("action", App.ACTION_SEND_STATUS) ); From 0a0f50ad18703c0da2da7603ffbb942385288059 Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Mon, 12 Sep 2011 17:47:56 -0700 Subject: [PATCH 14/69] fix comments for php server example --- server/php/README.txt | 2 +- server/php/example/send_sms.php | 6 +++++- server/php/example/server.php | 6 +++--- server/php/example/test_sms.php | 6 ------ 4 files changed, 9 insertions(+), 11 deletions(-) delete mode 100755 server/php/example/test_sms.php diff --git a/server/php/README.txt b/server/php/README.txt index cabf434..4736666 100755 --- a/server/php/README.txt +++ b/server/php/README.txt @@ -3,7 +3,7 @@ PHP server library for KalSMS, with example implementation The KalSMS.php library is intended to be used as part of a PHP application running on an HTTP server that receives incoming SMS messages from, and sends -outgoing SMS messages to an Android phone running KalSMS. +outgoing SMS messages to, an Android phone running KalSMS. To run the example implementation, the example/www/ directory should be made available via a web server running PHP (e.g. Apache). You can also use the included standalone diff --git a/server/php/example/send_sms.php b/server/php/example/send_sms.php index 821f638..e89229e 100755 --- a/server/php/example/send_sms.php +++ b/server/php/example/send_sms.php @@ -1,6 +1,10 @@ Date: Mon, 12 Sep 2011 18:16:44 -0700 Subject: [PATCH 15/69] remove test action to simplify server implementation; add icons from http://www.androidicons.com/freebies.php --- res/drawable-hdpi/ic_menu_link.png | Bin 0 -> 3512 bytes res/drawable-hdpi/ic_menu_settings.png | Bin 0 -> 3975 bytes res/drawable-hdpi/ic_menu_tick.png | Bin 0 -> 3283 bytes res/drawable-ldpi/ic_menu_link.png | Bin 0 -> 1579 bytes res/drawable-ldpi/ic_menu_settings.png | Bin 0 -> 1730 bytes res/drawable-ldpi/ic_menu_tick.png | Bin 0 -> 1534 bytes res/drawable-mdpi/ic_menu_link.png | Bin 0 -> 2273 bytes res/drawable-mdpi/ic_menu_settings.png | Bin 0 -> 2514 bytes res/drawable-mdpi/ic_menu_tick.png | Bin 0 -> 2250 bytes res/menu/mainmenu.xml | 6 +++--- res/values/strings.xml | 4 ++-- server/php/KalSMS.php | 3 --- server/php/example/www/index.php | 4 ---- src/org/envaya/kalsms/App.java | 1 - src/org/envaya/kalsms/Main.java | 6 ++++-- 15 files changed, 9 insertions(+), 15 deletions(-) create mode 100755 res/drawable-hdpi/ic_menu_link.png create mode 100755 res/drawable-hdpi/ic_menu_settings.png create mode 100755 res/drawable-hdpi/ic_menu_tick.png create mode 100755 res/drawable-ldpi/ic_menu_link.png create mode 100755 res/drawable-ldpi/ic_menu_settings.png create mode 100755 res/drawable-ldpi/ic_menu_tick.png create mode 100755 res/drawable-mdpi/ic_menu_link.png create mode 100755 res/drawable-mdpi/ic_menu_settings.png create mode 100755 res/drawable-mdpi/ic_menu_tick.png diff --git a/res/drawable-hdpi/ic_menu_link.png b/res/drawable-hdpi/ic_menu_link.png new file mode 100755 index 0000000000000000000000000000000000000000..e306517495dc47000888d0313470437b08e0f300 GIT binary patch literal 3512 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 T)$9Q9rx`#SS)lMpm)QRSWy+l- literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_menu_tick.png b/res/drawable-hdpi/ic_menu_tick.png new file mode 100755 index 0000000000000000000000000000000000000000..9691a11d89099db4ecc987277a2eaf969c90ea59 GIT binary patch literal 3283 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 RY9s&v002ovPDHLkV1jW}8YTb$ literal 0 HcmV?d00001 diff --git a/res/drawable-ldpi/ic_menu_link.png b/res/drawable-ldpi/ic_menu_link.png new file mode 100755 index 0000000000000000000000000000000000000000..44c54f842e91a4caec302905b8ca32c07dd6b9d1 GIT binary patch literal 1579 zcmV+`2Gse9P)>1r&7^QD^w`Om-rHus7uicQWwvsS98P*+#ig$EBFv~U~;olb{D zA^}N~ehvfz4dvzK6Tx7x_0F9;XH6y(!?G+Ai3BYxD_gyN`}W`G+mn3>n2@GvJIcz+ zz;PS^V0wBQs;c@_RV_Vp=1l4R`}c!BpO2Ac832$>Cdrd0Pd)&!=N|<+e*Aby)3hBv zpASZ(5e&n?U@*X9vBVt?M{_tFE{#MYL9f@#h@uEWW*Q-iB5XF>Af=Qcgk+wzH}j$} zot>SbiHV6FUauDhg8_sPh@z+}igKQ1+4^ueTs$%|67+aHjMZudr4*S=1`NYM6vba^ zYHB{859Wn|Iy*Zr%!L5}K@ehQv$?6Vvhw|KIK1}Wy?a5o+s*Ji|1`|7D2f3>$kakH zo*U@s(W4h0J$lqKw=#kt#CV?HR9RU$(9_eiX=G$%UKmM|8rQB}8_gHzxqv!4I&v_2 zy*>-0avZmL-MV!@5JEE1X!OXlU?fQz=6ODF?b@}SmoH!D^2N$)>W+?%^O~l$tXj1S z27@7+v1+kcHdj|ye@_U>T)%$Z8V-lOJkJ9FD5YRo7P2f4TdmeWZ*Ol;EEa2y$KwqE zY8G9g6DLk=)ikYjZe>JKe8h3w=GxlY8(DK!RaK=t9?!QXlL>?nn9XJ=iZX1mSQ`8K z`gSFg$>x%h5)eYFySux$EE-7DwB2sE8+yGS0AMzoRin|kDGPJ<>{&ek5JJ*Mqj9&Q zC?}UKTQ)37(kHSk*VWY2L{q8M7Ps3CqtOV5!vO%b3&otbWt3793Wd1Y@2n?6>({Tp z(bd)U+TOi;+lPjRyax{+{Bp&L6}#%{>P9G~doVNoDIp|t=+L1M$8p6h%Ysr0lgWhT z%a@NWb`BA{-QKU$=>PztC{CD6rYpye9ou>L?%gla>2#T*C?-`^--jQK1Arh1V;skwUT6-@TOmS7W?*38V@hd5I-M>unM`jC4GmR|j*gym zyWPZUwSo|WqM{-s5{V6z(!+!h3IO}}@4pz2$G3Sr9x%*I&ay1W1VMPy@As<<6k%Zx zRa8{OtyXIT%d#79-n{9FL?Wl0PG=4V01O5LN=iz;!R$rd*4B0@9*=Kxxm-E#$+8?1 zMRB9w?;prQFA4|%{C@wR;c$3$BoaC8bUMk?N4ub);5Vz)dWsN|*|%@s<;ls(cU&%4 zZg?t+5|bpUX%=SQR%NBjx1hbf{oS##vEZ{{Y&P4kk|eFKtE>C#@#Du=lF8)TSr~?4 zV6)j`f*>?CHa7nFJTO3>_t4?PhpQuz$alrX#SG8$xi;zbdK4BG-WCKQ(BI$xo~o)_ z2_a6G%LSv+2%Sy`o6R;ZNz%qZAn@EUK%PmhY1&6lr<1W*ETEKvQVP9ZkAi}N+kzkj zdV71jW3gDXD2ga5D#{H{S(e8IL1+pD0zbV746uNQD5cEGl`9K4j?2N!W`5gdvo+qj zb*n9vN;Ug@J~$kX9E_qU<2=tdZP>8kg<*g^6>>VA7bhkrDkdi zT5;m6#1L%K#jg#Nx)1fX6c=U)B#o&ddnj|Z$1>=%1;rsAgP4Q{&tsu1_7=K~(8VM; zVYQ@2(zP||O81^U#PS@^a_x;i%^w_eIY;03``zDn&Ud~G?AyNW+c#@uuMR>8a`x<5 z-qzMOR4SEzWZQN}cX#&$0NlHG?}DNzUvSQEEG;cfrBbO9Aw+y*goF@ec6RpLw{G3~ zeYsq2lw}z{pAV8G{R#l8)#^_z%R;46fvT#xo0L#nEXlZGIs;Z!rLQxdQFpOgWkj-ZQ zVB0oxT^HqY8Bd-(foYn^=ks{@@Zl5FG+#Y`{``(gNjohHA*7j^nGX%aXpv#xs{=v^$g+%RGzv;7bX|wf=WA(fZ2V9N@nb?rwH_wh ziBPFjiXAy}W4Tr-k zBO@d4C6mdNrfF|5#vp`%>$(8oF-iiXkE*I4jE#+r_V)HpOOkY>mNL#cOw)v67^qgO z1A8Wp}>4JRaz}&MnI#+pVw1-u6J%O&MeC z?$%(A;~~yn)9LiFTFN#L#@O9GDFgtrEH7?HXmxe< zOd^pe`ThPYwd-Re*!s2M_xrCT5{c64>gt*82+6X%xMxBhkLNC>bfdV{2Bq}W=;-LV z{{H?e!C>(HrlzK%uItcs9ZgM5#b7Y_et&=emC@1BbCl9k+xSD%w1qtpB7}%UBJmf_ zc@Dq}>Ckl@#bR-CczF1nrfCyhU0q?$`D>i>*Sfm8!kVT{3=a>VD;A5Bx~^lZKj%Dm z=+L1**K0}YR%#&xyLRo`PcoUzFH5D;=6Mtf1uQKsp;#>5R#o*AO6mMM@b}B*@|lK) zhF9C#+Ti#5LCC)$IOhn3LhlX?4E&l9;_QjgI=C`5HTAn}Hha8Ut!|!2sZ_$#r%#d3 z=ixZdV>8AO2m}xghrv02fs70C#NA2qB`cuWu<32)q*th4QMZ zZh)t(AInujKRj;Pd(Bg2CWhCr+IBqTasQGof`-1b{Lo9|hz)}Id_K78jt1sWI_a8682d>@O&K2{V3 z9LK?EG{R!B003URc!6Xx32n~f@%&g-RrOhWd;9EL2ReWL{J~HtbU_qF+qP}n;B-1s zP*4EI7zp`CMpacvl7xkY103;>dcT zIrjMRV??7-1OkEfqeqWkeQlu2moM8#M@RqkdcBUq!a`6=Hw2OuCj_dhLY8IB%*-Gf zjn387)RZ4Obm-YyYq(blMk0|*PN&o1a5z8+fublI8mg*-F$S~Q3`!|Dj)TMDfYa%8 zL?V$(ud2BwP2!Ku-axV}?-2xHNpY>9AP4}k0!F9PX)p)fZg-LpqU_kQV{Tz#p;yZh1Oc)v@5vj8 zkhr{u$#bQZoP`NObm6Zp}%F0rjPH%6oF&>Y1$g-Tx9?J>rfe{gV6Sgkf&i_~Vbskw~`AZ4@JjuAq{MbQ3Rw{8)RlF3s%hutP-A1`OP=TN=k|ysNrvI!;R9L@Aw=1~fBzLh5I(A`tZd7LIeYeO zK{y;9%49Otfj|I)AS~%Op6Bm3Ha5Q8Fc1JxO2=|VGMNk}CMJG3efspL%b@OV-@ZMq zee3M(w8UbuAyE`}1_A--_4*aNl2SUBmqP%+^ZYpQZx1oXuw}~@T_h6eKXc|xa}HG2 z{$Ma@4Tr2GRk8p(BbN$s^r0bpoosAzh6dMqA~ zZ`E7^07;VY=+Ps@Vll~Xx8F%7la-7yx8LuF!C+YRHNbHkTrSts($dmhb#--r<>inT zwY9Y~Mx*h#$z)Qsn_E>?P)gx;yHQqF#^&bc8W>}4pU(%qUJn2(?l(dROeT|RG#ZcB z*4ECvs^+>68&y@=jT<*Q6NyB}vuDqUBuUx1#p57*qp#)+V+?k?T{WA{J(|?5S?-L-<6Bdy6qX&DOGOC*Aw;`so>EGWwY0Ph5JKejYHj#Ut*R;+ z85t>j^5jWRI-PDyrBcXbGRR~y*+J+a2$txK97}^6)Y?)eErd*N528^&2Ttea5|l_bUGc}v112hSq9JZ$mjD& zrBZnEmWc7e;HXB4ygvaB_Fvfm5Jw44SitbUGcZuC9i|;Q+^RFio@cjNNWWO-;?;0Pp}_u7PMY8b~A( z6Otr(HBDOsF9-r8Nm{hq?UOc}?OJVZ?T6#z<5#~7zG$}Ka=Cu;Eci_uh(@DD2lO6CKIfxs)8)b8%Ex4 zx0{kAg$^D(I0_(J2?zJ0@bmNYUkFbreXwiSu2<{o>ShTcc~KO{bX^bn{r)xZ)`#P` z_3%0&#Qb7{O&Ay%83`nl$qB~TnzS)Z6Fkp@G4^2h?%l68G&Cd#A^G9q;qU9Ze!}nf zuSpy0LkO|bCUoe~q0yT+Zx$+re{KU7ycK!7-Cp{xm^Syz;Q=6>PJhqm^L=ULWmyij zx3`a;J9n;sYHI3tmoHydw%ov_OP87xiNpkB?7!fPb^d-sLqlgdylI-es;ZhS%OHeo z2=8z>LhbGCqmfADo%#9skgn^m#N+V~E1jV#G7yPGt1OcKb{=UAx zeof?y;=Nw)p9DdGX__b$3MF`(%~l@yA3lBh^pxN4M@>x)^7;IM^XJe1V9N{`h7qQe z)>=7lsTlcB>g($}8yg#w8XYY|CM@9kYhGBGSng*B4 z1xb>isw$S3m%p=R26DOF0k_*-f;UYQgb+|lKWS`i>?()9aN&a2dX5m1KXBl{XEil7 z-%=Fix~i&wbi3WZR#o-r;lqbV0O-TR!{1N zzuz=X^Li?k3Q|fTNfHdh@XgQ9|2z_j{5up1#flJXK!%2fnv%)n$C4y@%TJc$I0%9O zMNw{465xtDvD@vwu(Y()h1Dp6F_w{KIWj#xjY6Ri%VaXa>gsApk_3+9P$(3zyu6H+ zm6f*v#MbXOG&IySH#availXN6c);`g8hA;P?sj%|M%SC(NY8u+2qF27j*k0^qP$Bf z1!GKiI2^CHwzmE%7K>febv>wQ8Wcr=&1M77^AH394u`|aW4?U9NF=g19*<8_N;Qwi zQ>tjd>dA_tI8|R?{~CaFB@M08Stxt_`0;md+_=FDg78^OOUnn*X!L3(lL=Z)%Bu60 z>~=d`F4r{5M&iN2!99sY;x1!MQ&qK86pA?yNs^}a?Ag;92n6N`A%%@V)~n(xWw9dI zrl+SdHa2!GlgR`%O)HJWH`Xh}9Dy z%korHQ&VRk5SXoG3N8Isr3;I9*qpBGF~cx|KA#VgBmuyhkysD}tHbtobadPygyaVX z1{&w)=I&ZKZ>0?(1U%0}lB5@c2Ud@_B89_YPS^EVHk%EW&o<>M&O@ft=?rysb&UeZ zP)Zw<$>cpz6n(0yu8F+eZig((Q-MI>h2Vkbn1wQ#%vd&?4QiTJnkvg@8=mJuDK#Aq zhXt>P!{H-|MB*M}%(p)0iye7W@IZwrbmq*NHloc zX_|)W>T0-Lu2Pe-%x)z-P}vFaKA-OoD=RBCxm<2d8ZgF8m&+9j27{vj@@}`=X8jvi zG{3sR>U|foEPvYC+PY=%KxM-mfBW|BmYJEEk9A#l=5jgf>X>&p9H&m4IB^kxUR*9- zzkaF6io<|laqUAXJ=0X*bCrak|e!% z?AWp20?1k|zu4^Pty{OgmP)1GFio?;G|jjm2zR=>yT<^mRN7&;)Ibq#!;yIXyizp5 v71weAOaM6mx#zj8Z);oI+SazVRf+xsnJtdNX4CLU00000NkvXXu0mjfD(zQG literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_menu_settings.png b/res/drawable-mdpi/ic_menu_settings.png new file mode 100755 index 0000000000000000000000000000000000000000..c2cc333f9279e11a5c5cd553ff2d0fafc337376c GIT binary patch literal 2514 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

\ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 8dc1820..5a5657d 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2,6 +2,6 @@ KalSMS 2 Settings - Test - Check Now + Test Connection + Check Messages diff --git a/server/php/KalSMS.php b/server/php/KalSMS.php index bc11040..9e4ecee 100755 --- a/server/php/KalSMS.php +++ b/server/php/KalSMS.php @@ -11,7 +11,6 @@ class KalSMS const ACTION_INCOMING = 'incoming'; const ACTION_OUTGOING = 'outgoing'; const ACTION_SEND_STATUS = 'send_status'; - const ACTION_TEST = 'test'; const STATUS_QUEUED = 'queued'; const STATUS_FAILED = 'failed'; @@ -50,8 +49,6 @@ private function _get_request_action() return new KalSMS_Action_Outgoing($this); case static::ACTION_SEND_STATUS: return new KalSMS_Action_SendStatus($this); - case static::ACTION_TEST: - return new KalSMS_Action_Test($this); default: return new KalSMS_Action($this); } diff --git a/server/php/example/www/index.php b/server/php/example/www/index.php index 3f5a1b4..223649e 100755 --- a/server/php/example/www/index.php +++ b/server/php/example/www/index.php @@ -80,10 +80,6 @@ } return; - case KalSMS::ACTION_TEST: - echo "OK"; - return; - default: header("HTTP/1.1 404 Not Found"); echo "Invalid action"; diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index 3388ee5..e7b6edb 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -29,7 +29,6 @@ public class App { public static final String ACTION_OUTGOING = "outgoing"; public static final String ACTION_INCOMING = "incoming"; public static final String ACTION_SEND_STATUS = "send_status"; - public static final String ACTION_TEST = "test"; public static final String STATUS_QUEUED = "queued"; public static final String STATUS_FAILED = "failed"; diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java index 686a7e5..9f00c0e 100755 --- a/src/org/envaya/kalsms/Main.java +++ b/src/org/envaya/kalsms/Main.java @@ -40,7 +40,9 @@ public TestTask() { @Override protected void handleResponse(HttpResponse response) throws Exception { - app.log("Server connection OK!"); + parseResponseXML(response); + + app.log("Server connection OK!"); } } @@ -139,7 +141,7 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.test: app.log("Testing server connection..."); new TestTask().execute( - new BasicNameValuePair("action", App.ACTION_TEST) + new BasicNameValuePair("action", App.ACTION_OUTGOING) ); return true; default: From 307f354deb593fca4356420a68682e6934338adb Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Mon, 12 Sep 2011 19:06:03 -0700 Subject: [PATCH 16/69] add help screen --- AndroidManifest.xml | 3 ++ icons_license.txt | 3 ++ res/drawable-hdpi/ic_menu_equalizer.png | Bin 0 -> 1987 bytes res/drawable-hdpi/ic_menu_puzzle.png | Bin 0 -> 2275 bytes res/drawable-ldpi/ic_menu_equalizer.png | Bin 0 -> 969 bytes res/drawable-ldpi/ic_menu_puzzle.png | Bin 0 -> 1145 bytes res/drawable-mdpi/ic_menu_equalizer.png | Bin 0 -> 973 bytes res/drawable-mdpi/ic_menu_puzzle.png | Bin 0 -> 1571 bytes res/layout/help.xml | 19 +++++++++ res/menu/mainmenu.xml | 7 +++- res/values/strings.xml | 1 + src/org/envaya/kalsms/Help.java | 49 ++++++++++++++++++++++++ src/org/envaya/kalsms/Main.java | 3 ++ 13 files changed, 83 insertions(+), 2 deletions(-) create mode 100755 icons_license.txt create mode 100755 res/drawable-hdpi/ic_menu_equalizer.png create mode 100755 res/drawable-hdpi/ic_menu_puzzle.png create mode 100755 res/drawable-ldpi/ic_menu_equalizer.png create mode 100755 res/drawable-ldpi/ic_menu_puzzle.png create mode 100755 res/drawable-mdpi/ic_menu_equalizer.png create mode 100755 res/drawable-mdpi/ic_menu_puzzle.png create mode 100755 res/layout/help.xml create mode 100755 src/org/envaya/kalsms/Help.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 023fbb9..ac1e36c 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -19,6 +19,9 @@ + + + diff --git a/icons_license.txt b/icons_license.txt new file mode 100755 index 0000000..5b6e2a7 --- /dev/null +++ b/icons_license.txt @@ -0,0 +1,3 @@ +This work is licensed under the Creative Commons Attribution 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. + +These icons can be used both commercially and for personal use, but you must always add a link to www.androidicons.com inside your software or your website. You must not resell any icons or distribute them in any other way than referring to www.androidicons.com. The Icons as such are the property of the author. diff --git a/res/drawable-hdpi/ic_menu_equalizer.png b/res/drawable-hdpi/ic_menu_equalizer.png new file mode 100755 index 0000000000000000000000000000000000000000..553dd69221560d4d3deae19a70810773aea64cdc GIT binary patch literal 1987 zcmV;!2R!(RP)*W{2xP6;6w=MrM@Iv9B) zA;}V>>F(+2m&0_6*>;bnYkCyPrhm`}wW|L8>UCB1oA(MRDJdx_DJdx_DJdx_DJgli zV1p33fB*g)D=RDS5mD7~oDXMbXaAw9ar^e|?`&*r{DLvYT-W{Z(xprPRMoh3>(=)+ zH#g@QV_T!6qaU0~IUxjdT^FwFLI}Zp-=9;_Xfzt{YMKVu zb>TP;IOlw8Yim|*G%uqS6_8TCH8wT|!!Q5=!M41{4wDjK$JQ&Ekfp`k2|mWoCeg_al*G0ULQ7Jbj4*a6bAER8V+##nM6Q4|RP zIuQkou{<+@lD_GCdFgz90_R*wBa5O-%rFc?OWqpLHZTlB4!{J^M2>-`9Mkn)KRQJz z>LCBjnKM5EU;z;Ey$FC8U%4$VI=H=X4p6{Bj4>jjCV+JSFH-*|08!56b)Zpp(Bk5v z?l{idFJ8R3xUjG=*=;jsjM3GrSO4oc&VA1L@8;*{8{ISq@%E!A+S!K&iL!&Fl=IJ@ zKmSb-1U(1l`~KHF&s!QD9sL%7%iY=yYI&q}2BE9$pnARjGs7_8x-N8G@A}q=2+d{_ zzVE~L{kNBvmafdr&H3Hh7Ue+fK}5u^U%&p%YPAa6wjqQ-=O@0S9f%06RtuizAq>OA z)oOK;i2j|mFTEBHiuW2V3Fn0nJ^M&0q3b%LD8k6d2uJ(Japp+U1618l_T52f4^AFB zbm%F7$ta2tMNwD7l9?4R!>yx7kFKLj_8^L04tP}uJg8VIjf#T^0CODYlOPBd66f@C z5a%31h|f=+Jh_dWv!@>p!VCg{U}|dW&f~|AWe@~2048>8Ohl_v%0Em@Oe_Hi@H#sv z4h+M|$;su($;oQ~J^&!mJ?TaOLI4c_+W?xcV&JGWgF2$;O#oZyosp#E+RZty#SBV_ z2fAa;;^N|8*VotIY&M$!03ig1hliK1UAy*m72Tp=u`G>!2^6#FG$c-UDD@x2S4{Pp zc`59m!-o$Cwr%ei$T^3UQYn-7S}n0Ol}aV7R4O|L#!I7gP$yYGH&oaw2OY>gs2{RY z6@!SR2SH&6?NzE`zwAhJARKg1G_ojMS$*9pJcF|KkNrwjyb^(8*59iA`(dvF#llN0 zNqM9%h5jrCsf*>U&L-{B8lC^^mwnx&W8-zxG)1~POGLS(J(=>&qX*<=~D2fnV4CL2 z(9qCFLqkKqt<`E9sv7Z8m}SRtKC*55My*!cR2ko%>_NR|)!N$Hm5q&!_j^@mi3p8G zV>ArID*$u=KUdX=Yjfja7{1?ZHaUP_=NeedK=vS2!Z18nsZ=mLJPaviuIeliy;G~z zep##4LRF0*2;g}hJkR?H{+|vaBDS=&#M0B7F_y$PSyfcFxVZQY&N)oegr@CoLKued zeIKn>Yvjn0BQ6oG0f4H8=Xt2t>*L+*>PFEMW||O!0r*NrF}|k}QP^or(#LU}+&LsE zC8U%fB8-fTXlU=7LPbN@b!eIf(=?G~*KQPDVw!21{|SNstyVjUD1?BN@@cFcrrI+A z(zfj{q9{5VhGEa22to+N@y*JqQ>VOsG&tul3wfBa-a>M~VK literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_menu_puzzle.png b/res/drawable-hdpi/ic_menu_puzzle.png new file mode 100755 index 0000000000000000000000000000000000000000..f999360c726239bf79b8fcb3d18403f9e8057cf9 GIT binary patch literal 2275 zcmV<92pso`P)K~#90?OI=m9akCuopWZ++_`h_-eotNK)|=q#29MzLG40W z2-YMk`%u=!q>-jY?Sdj92m#TM*u<9*D~3w6HK2L%r3SSc1HJ^7q}CeK5_~DXlKy)$#>d_LScxA)%cpP9|h_BPB94$Iz|`Q|&n`Tm{n9H2mf0tE^bC{Un4fdbjV zK4}G3!if_nzUuq_i@xuFQ3w%xo_BL(WaRh9j~~CjnR@gcK_^e1EHTD@)o3(cQA%~` zXWMo>I5_z0p`oE;)6>(@X6R#W#!Irmah%_TVfa$5Rzta5hLjRSgfI-Tw6r9GAo#xT z`y9acHbbAjvY>P4&OJ{=S4*W5uU4xll}fOz);Cm2q0wmI;lqcBqDWV()#s+Cr*Gt~ zQ(sw->$-<5%i@(v1+MEt2+_V~+cs?5hLjS%?{nL>Uj&dlK|Jplqy-T%&+|UtNe*MI zYgm?ra=DC3r2;ACGenfT2lRynB@^YI=UJTdHaWduj6nzi*L6WeE?OTr&QrI(u^<4# z^SpPZl;C`2wK(UHQleBU-2}k%wxWI_2(?=6k`O{IXFUdtF>ubw^SnQ7hEDxNkRBZ! z{eW}+=2|y0#@-$q8~Y~!%^0IRmFkNa#26zYiUEXS7~Wd#Uad8>*0%wK0Ce8EK@&o@BPv^0Cgj<`&@2oH_IA;^N{@!Z37~o9rWs zFb3Ck_13Lh<+g3x20hPvBv@;$(P%U}xN=PYJd`3!%p`jsoo`*P2R{{|c;y8xZTBe>rg4_tI)oL+-)Z`d+ z61trGBo+iIv&~g&4NBvWJ;;q9lYX-j1bplgYsk+ebG71gHRPV=*YVN%3b_*WX(tEB zksz&eui-WYxe@dvSPxJTv?iW7f;zL?M(FUw5X3oeSK5p*5Rs{UQk4xW;c*FalD|Rk z1*KG`Dtf&zqzEA(glH240C60f3c5Xrh-GP!tcM_R`0(LxNhuFomh}{4%vMU>R!Y5j z>C&Y?A!$JbLBQAQ4n8hAd)3LfQ7V=GT&vYS6hhF#!ort)-~XmkYL6icfFPncElHUO znwXfVZr{HBcdqNcz&U4T@^Kt*S4w^D$dMy|RZ88iR;$f=y*@ZFFaXQ4a)&lQo4dj=TtdbUMwXk|v17;Ha$Wb^uIs{e-S(bU z6h-iTAHMHHDTQ*mjFFKM)M~XA8}J(eAp|VTLa9_bIWaNubF`XhF#tkqZXAG}3?$bh z##rh`e`*Uldi3aXgM))FcWk?WbKVYyl}ZIc5I`x_-kw=)(YX;wg72--(a~Q3Xrj9X zO8|7zsWdT?vbJbb6Erk5^z}-mA`Cg50oVjd$8kVJZPPcC$O}o}eP?)hxR;zZlHb;? zM5+i%M66n^KJ9s4S4`o|a?co>B_+di^UKPs_(hOAtAZ^A2My>~x`CVvL*h zRXN3^W%a92~tWyDTOGC+9XLSF)%P-p4;Jh-VUWyYjUkB z8|88twr#_*to(O1l9Z~+hcD-2xyQjm8%M><91+ z5h04Aw#zw=(>^y8h9RP;b-vTGtoA-zLiXlu`>a)=uE({^!c$85v#F`6H_%!>n03?N zeBXcl>eZ`9<2XLfIk$}!8DdQ}--*ul$9mR;x%DE}o0X)LKc1SJdL6(+vXg)YMdEE!ksZW2HEbUs{hvP{w1!8*V{~0v2zf^Y;~>(OScGU1+U$?B2cm zI1&AHxxR5CoA2Gb_f;Xp9@lkY+xDt4sPkG9(DU2X$N+BJc9^zdPeko5fYy2!0E>uL zI^t^&4-fAmBG|UwRS2&2jwp%(WL$sdMNn+p_8V~=pJI%a*P1}jj_3od(L;7!x2}|W zxYAk1UL;vEV2qtd#P@up@;r|Cws*L#R(1-v4002ovPDHLkV1l`=Rowsp literal 0 HcmV?d00001 diff --git a/res/drawable-ldpi/ic_menu_equalizer.png b/res/drawable-ldpi/ic_menu_equalizer.png new file mode 100755 index 0000000000000000000000000000000000000000..a2252e43120cd361482f8afee0b9256a3797c35f GIT binary patch literal 969 zcmV;)12+7LP)Yz`F)#!gvukS?7%X0OFK;-7zcigD;~62sZ`$AYPBWK`8NPac`+v^CuP$#r-z1yN&xW8i#a?z{A8Nu zTo4n=kR(Y@#>dCOG|k7wV)4GN>y_^Pot>R%wOXBy$K%j!Hs}1}B$LUxL?RK$gfcWT zGD3(5q9_6p{c^qM=jSL%5+EYTvg{j2mgSyIPeVi`mdoWKK@h-k9N?S}6A>aJ-Qj?U z{KAB0dKyB+4iTl`rt#Z%&A7>WDXbY^Lxn=2P^na2=5o1+yT%x!d_I4W$z-1Dx^BDc zj^p%95{B_LR4SDcM@L7wsi`S02ts#Iyu7?zsMqVy0pLU542_PCa#dBqFpOJ)+@_Qy zDNaNT5uLsmnwgntHX4n0jIkGpD7x!JM7C{z$z(D+u9>U#o=N_whY-;sq7?whdGHP* z-g;@>(l)EZEbBWcX4s?%CfAuyT&!Gbf-ol z;;O20S(YIdi(P-REQ6}5oQQZ3lTN2O5qTCPs6tFpl&_smCoPI%Wp;M9+BKlj*x1;1 zGMQXqj78laIF4gFjP*mIgo{j rb^t>FV7tt3i-?%lfIrYc1BKL2Ov1G{hX`3-00000NkvXXu0mjfkd3>_ literal 0 HcmV?d00001 diff --git a/res/drawable-ldpi/ic_menu_puzzle.png b/res/drawable-ldpi/ic_menu_puzzle.png new file mode 100755 index 0000000000000000000000000000000000000000..84b05d00306fb78cf84c3d6578ef2c6d49ecf146 GIT binary patch literal 1145 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 + + + + + + + \ No newline at end of file diff --git a/res/menu/mainmenu.xml b/res/menu/mainmenu.xml index 13b4fdc..1574d9c 100755 --- a/res/menu/mainmenu.xml +++ b/res/menu/mainmenu.xml @@ -1,12 +1,15 @@ - + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 5a5657d..acbed8b 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4,4 +4,5 @@ Settings Test Connection Check Messages + Help diff --git a/src/org/envaya/kalsms/Help.java b/src/org/envaya/kalsms/Help.java new file mode 100755 index 0000000..9c43ff5 --- /dev/null +++ b/src/org/envaya/kalsms/Help.java @@ -0,0 +1,49 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package org.envaya.kalsms; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.text.Html; +import android.view.Menu; +import android.widget.TextView; + +/** + * + * @author Jesse + */ +public class Help extends Activity { + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + setContentView(R.layout.help); + + TextView help = (TextView) this.findViewById(R.id.help); + + String html = "KalSMS is a SMS gateway.

" + + "It forwards all incoming SMS messages received by this phone to a server on the internet, " + + "and also sends outgoing SMS messages from that server to other phones.

" + + "(See https://github.com/youngj/KalSMS/wiki " + + "for information about setting up a server.)

" + + "The Settings screen allows you configure KalSMS to work with a particular server, " + + "by entering the server URL, your phone number, " + + "and the password assigned to your phone on the server.

" + + "Menu icons cc/by www.androidicons.com

"; + + help.setText(Html.fromHtml(html)); + + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + startActivity(new Intent(this, Main.class)); + + return(true); + } +} diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java index 9f00c0e..cfa9a5c 100755 --- a/src/org/envaya/kalsms/Main.java +++ b/src/org/envaya/kalsms/Main.java @@ -138,6 +138,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.check_now: app.checkOutgoingMessages(); return true; + case R.id.help: + startActivity(new Intent(this, Help.class)); + return true; case R.id.test: app.log("Testing server connection..."); new TestTask().execute( From 29111114906e5eb0d579016aba9552f86a343387 Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Mon, 12 Sep 2011 22:03:25 -0700 Subject: [PATCH 17/69] keep messages waiting to be sent in memory; if forwarding fails (in either direction), retry a few times (1m, 10m, 1h, 1d) before giving up; add menu button to retry sending now --- AndroidManifest.xml | 3 - res/drawable-hdpi/ic_menu_magnet.png | Bin 0 -> 2310 bytes res/drawable-ldpi/ic_menu_magnet.png | Bin 0 -> 1078 bytes res/drawable-mdpi/ic_menu_magnet.png | Bin 0 -> 1399 bytes res/menu/mainmenu.xml | 3 + res/values/strings.xml | 1 + src/org/envaya/kalsms/App.java | 351 +++++++++++++++--- src/org/envaya/kalsms/DBHelper.java | 43 --- src/org/envaya/kalsms/HttpTask.java | 20 +- .../kalsms/IncomingMessageForwarder.java | 75 +--- src/org/envaya/kalsms/Main.java | 23 +- .../envaya/kalsms/MessageStatusNotifier.java | 65 +--- .../envaya/kalsms/OutgoingMessagePoller.java | 1 + src/org/envaya/kalsms/OutgoingSmsMessage.java | 19 +- 14 files changed, 368 insertions(+), 236 deletions(-) create mode 100755 res/drawable-hdpi/ic_menu_magnet.png create mode 100755 res/drawable-ldpi/ic_menu_magnet.png create mode 100755 res/drawable-mdpi/ic_menu_magnet.png delete mode 100755 src/org/envaya/kalsms/DBHelper.java 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 0000000000000000000000000000000000000000..c1f5cac5faa0015d5660325527f031f2970e28f5 GIT binary patch literal 2310 zcmV+h3HkPkP)-v8j$GO26 z`{~%&*vC!%xFpg7oj7sgyM;pG1m_%#F;GfD2tgQz5JJH7JP0A076||dA+=XR2q>kY zX__C}w*8B2Hv8_OLx=v=bSF<5gX|5*j~~Cz7~7f6W?@+t7-O~XIOnKVtMGllX@P_g zwZ=Jz=Xr2l7ryTU08~}=vf1q0ot>RW4<0;NXsWZvki>#SE|*gr$HBI=7Jz-YEZG820?%z2p$lrTCHMfX$jS86`XTqGMPdymmBZx?L9FvG9sGlcnwt9 z8*;gvVwxr>r2w#i2SpAeS7>@*^%ht!nD6^=T^HqY8O34|<#IVUJ3BjGtycG)KY#v} z!-o&wZ@Tk36xkar%Yv#d>q-a_gb=^3iTa7UnN|-W1d5`7QVLDeVB0qG`8+mn-i*%9 zPFR+Oa=HA~+1c4Yoj!fKzctbvxJvnF(&d7dn|!w`fJFvg(kIv8V#C-Sv|L?J}YZ5tsL$uvzA zi^b0bLGWz=KTI+nsi%nCi~%sP96?jr-*^(!)6;FYZ{HqrUH5Cg?+*$gw)wsf&bgFG z@jO{97E!HM=WN^l{E;I^7G=jHWkH0HFbqQuz()f2Y@~~xo}T%ho}P~Zyayn2<;s-< zOG`^X0ATvQ-|$8_wrImJAcWW?gg5};E!hDjiGxByNU$n`ZdKy|JOC8{B>;ut;o%Ei zU0vUxl>RNAlYqwNB|-=?nG7^dLl6Ytlzmp9#qQB+B9~SGXfrc2`D@p%z0Wz{B9ko; zLZDKqpi-$UQA#(Bj*hM!pFD!~aFdo1S*rlxc6WEr+qV6k=(C1bwm9OZlv+U$?2#SA zb1hIzB$Bsh&z>uaqFjtWZ&aj6Ao#xjl5F1_LZAo%!0Ybr{=Q744y^3k2q8VPoo*O` z2q7T=e{gVcnh-KAc`qoXV2o{+?RUcn6cGXl!Z7?xlKq;ZDF5FAMOG9DA*N&nimYgJ zv@$l#h7t$>2q8fb1YsO7HflWpKq-|zNs!J-KM%OB`{j5u+vuJzvY@+IIbdjr4Ji=k z{Dmi_epeG^LT6F6Hk}fkj7$R{ zG86TNvY_$t@vl@Wl{eyJC!n#cV=ShoY46MSxnV45a&nTEN~N~}(1e-B#hp8M{t?TE zn+^Nsh7jo3v16aTapT5y-}k?yXH-r$-bshC9N?du-OaWE2>xQoD&~+WA zX+qa^WHK44WAC`wfl~VK;o;$Tu$)IVNeKa^Y zcm#lp=df%&iC2vzF80{(n_0n<5?f7F zbvxJ5(Q#n+?%jV!6Rs7LI1YN05KmK}x+5Jh#-OU|Kelb#_WID!&_`Hyt>&spk)VJo zUTUAEK(R3KscBRh^1=4)+g}+P8u}}MDiW?*t7+JRq7(sD6h#Co`oPw(Bk_kv|vMF3v2ZdH%0i&0foKj56d$T`R8=xDgOxL8YyTb2da zbz34(eNY(3G_g3*dso->cXsU9ak{_1{~lJXre)#)3FaWvG%r^wl@|@efNk3#gw!g` zEX%4zzRfQ6u|RQ)g2hl()jw;R_M6_`-uHIx+VxK?7drR=f>yZHL*r=l!q~;`+_{s# zdiCl_&iOvqb?vy2AlflQ*Y(=}Z!|$b2$?5@d`bxUgb?xxW9-8%TekdqU|?Vx%M-AN zbrdEhX#tfEK*Kr|P+?`i6h!Yu^xg-+>;9HXd?iKt;UbaZvXu2ET>bhqL`g|WNl8gb gNl8gbNm*z92QTp`sq%_iivR!s07*qoM6N<$f??)Jt^fc4 literal 0 HcmV?d00001 diff --git a/res/drawable-ldpi/ic_menu_magnet.png b/res/drawable-ldpi/ic_menu_magnet.png new file mode 100755 index 0000000000000000000000000000000000000000..00d70842cd4977862b51e6e8a6dda96c4abc8680 GIT binary patch literal 1078 zcmV-61j+k}P)DhR$t0PO$QV$u{)q=Kg)L#BcqsN# z8mZv9rEF0ap~XE*PlY`Q-g0PhLA30l6}DK~i-)C$+Cu56Qrm-wLYgMDDKzRhCR68o zJ?y&ZX1=)Fb@$Tz;A0MN-sAUv@4fGPKnERk@IM2o48>ycqf)6f&&&n@1VI31RtF-2 zWm#3*wzuN(_>YN1;`@Ptfp5ph#~W8g=rEhjCj0vO007l$6;jH!!J5q`j*pMwc^*8^ zLl6YN_xASQn304Ch!4P*QsT11%wT2^5rhzk$Kx0t9wuGa->pBtK zHZ}s!^R&gq#a{qmXlMwUrYQrRCv?8oG);r!IEck!FV*YyPXN5rj!uz*4#VPvmJ9%h==4d5oI{hD@9pmH zZj?%;&l`=#o4)T~_(2E($8j9r_g@3>byyH(Q~wtbksKKrIkav2J;N|+%-quSx~?M# zg6panS4!v%`Fy^{%&V$NB%)VdC?O(}p67iR1VQNaAcVN4@>TZGv!c;x1gfdDEDOv` zMAY&YJr{)lz;)eM!xUm>7>2P0#q>TGg%%bTQl97CG))siT+Eq>plRBc%J<3@T3T8% zT-W{7G)?F6DSOyIh{a+*sr;`@p_P@DXti2>eP?Iqwr$(jB9TbTGa`h5ZQFlkv)ONm zD0FpD-oo?q^B>pi^|t|N0O$$!A=9!f&2b#qw%t-HAq0}iB+}{hjY6StS4Bvvkgn@D zbGe+B%jEz7dwY8bfpG;AAfhG_HC2~f*jNJ(rpYu-ba!`i zI-S1P-`{^dpU?k!SwgDyLt9;4{rlkHAi2H04FDJ&9c{Z05fOAn)`}@sr=Jy+*PoF-0`t{rhXin8ZIZ#90%EK7Fz2sZr!@|QBnb2_na}tdGO%Dqm7M?Bg@On z2!a4oN)(GlxUQS%2ki@ubEn(fdcBTHrShlaI74G&W1-EyEeCWim%Ao}_&S@-zH5vD z5kX1`qGb7IjDb?Bb@JHm5FrFS&wD2f!_NRr+w9vSKmY^)2MH2>JNwJa%ekecrT3#K zI$5vRF9ku6snu#7MHpich9RoeYU#?AEBkE;-Se^4D@`YG0CI70@z9eePp((1)ssOG zblgr_YXm_6t@ZJ-v9U+p>|5`FmxYK75h)_t93CEC93CG2SP1b4W2|FR6A@CW6qHid zWPa@e+94t}I5_ysb=^;5?{^bHMDN*bzEJ^9qSL2O|EjhAOKXiz!x#h3dC_L!larH!8yg!xan27h#=4$B zj4`;b`vZWkVS4RbXliOI8%5EHMx*h;`uh5(zVBx|&jax9GChWK4k5(TbLY93UcStRDTrT%zu~=LuB5RnfNgVIjmuc#g zbYyX}pH8P|&YnH{9TIpSNZ8bG#dd!8Li|#ZPN(6z?)T@< z`0(`5(9qui8oj&+#n>Vc6$*t3$8oOb^ZA2uK6ER>w!r`h06YWm6u@FSo&NFEsZ$TL z*=)7B@EyODC28tY09?HMt*x6w#bYNP@6)GGpFVxs`3HpUm3;5hY0v-w002ovPDHLk FV1h(mj%fe@ literal 0 HcmV?d00001 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 static App getInstance(Context context) return app; } - static void debug(String msg) + public void debug(String msg) { Log.d(LOG_NAME, msg); } @@ -174,60 +239,166 @@ public int getOutgoingPollSeconds() public String getPassword() { return settings.getString("password", ""); - } + } + + private void notifyStatus(OutgoingSmsMessage sms, String status, String errorMessage) + { + String serverId = sms.getServerId(); + + 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 = sms.getLogName(); + + 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 SQLiteDatabase getWritableDatabase() + public synchronized void retryStuckMessages(boolean retryAll) { - return new DBHelper(context).getWritableDatabase(); + retryStuckOutgoingMessages(retryAll); + retryStuckIncomingMessages(retryAll); } - public HttpClient getHttpClient() + public synchronized int getStuckMessageCount() { - HttpParams httpParameters = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(httpParameters, 8000); - HttpConnectionParams.setSoTimeout(httpParameters, 8000); - return new DefaultHttpClient(httpParameters); - } + return outgoingSmsMap.size() + incomingSmsMap.size(); + } - public void sendSMS(OutgoingSmsMessage sms) - { - String serverId = sms.getServerId(); - - if (serverId != null) + public synchronized void retryStuckOutgoingMessages(boolean retryAll) + { + for (Entry entry : outgoingSmsMap.entrySet()) { - SQLiteDatabase db = this.getWritableDatabase(); - Cursor cursor = - db.rawQuery("select 1 from sms_status where server_id=?", new String[] { serverId }); + QueuedOutgoingSms queuedSms = entry.getValue(); + if (retryAll || queuedSms.canAttemptNow()) + { + queuedSms.nextAttemptTime = 0; + + log("Retrying sending " +queuedSms.sms.getLogName() + + " to " + queuedSms.sms.getTo()); + + trySendSMS(queuedSms.sms); + } + } + } - boolean exists = (cursor.getCount() > 0); - cursor.close(); - if (exists) + public synchronized void retryStuckIncomingMessages(boolean retryAll) + { + for (Entry entry : incomingSmsMap.entrySet()) + { + QueuedIncomingSms queuedSms = entry.getValue(); + if (retryAll || queuedSms.canAttemptNow()) { - 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(); - } + 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 @@ protected void handleResponse(HttpResponse response) throws Exception { } } + + 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.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 HttpTask(App app) 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 @@ protected HttpResponse doInBackground(BasicNameValuePair... params) { 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 @@ protected HttpResponse doInBackground(BasicNameValuePair... params) { 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.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 void onReceive(Context context, Intent intent) { 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 void showLogMessage(String message) } }); } } - - - 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 boolean onOptionsItemSelected(MenuItem item) { 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 boolean onCreateOptionsMenu(Menu menu) { 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); + App app = App.getInstance(context); + + String id = intent.getExtras().getString("id"); - String serverId = intent.getExtras().getString("serverId"); + int resultCode = getResultCode(); - 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; - } + 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() From d29e7b2c5857f6961e85d898136311c9333c85f9 Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Mon, 12 Sep 2011 22:52:45 -0700 Subject: [PATCH 18/69] start alarm to poll for outgoing messages when phone boots; add option to automatically start main activity on boot --- AndroidManifest.xml | 7 ++++++ res/xml/prefs.xml | 9 +++---- src/org/envaya/kalsms/App.java | 5 ++++ src/org/envaya/kalsms/BootReceiver.java | 24 +++++++++++++++++++ .../kalsms/IncomingMessageForwarder.java | 2 +- src/org/envaya/kalsms/Main.java | 4 ++-- .../envaya/kalsms/MessageStatusNotifier.java | 2 +- .../envaya/kalsms/OutgoingMessagePoller.java | 2 +- src/org/envaya/kalsms/Prefs.java | 2 +- 9 files changed, 45 insertions(+), 12 deletions(-) create mode 100755 src/org/envaya/kalsms/BootReceiver.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 542e101..718ee60 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -9,6 +9,7 @@ + @@ -33,6 +34,12 @@ + + + + + + diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index 2ba5cb1..7ff56d5 100755 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -27,11 +27,8 @@ android:entryValues="@array/check_intervals_values" > - + android:key="launch_on_boot" + android:title="Launch automatically?" + > \ No newline at end of file diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index a1b4c4f..cf79b3a 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -236,6 +236,11 @@ public int getOutgoingPollSeconds() return Integer.parseInt(settings.getString("outgoing_interval", "0")); } + public boolean getLaunchOnBoot() + { + return settings.getBoolean("launch_on_boot", true); + } + public String getPassword() { return settings.getString("password", ""); diff --git a/src/org/envaya/kalsms/BootReceiver.java b/src/org/envaya/kalsms/BootReceiver.java new file mode 100755 index 0000000..407ed66 --- /dev/null +++ b/src/org/envaya/kalsms/BootReceiver.java @@ -0,0 +1,24 @@ + +package org.envaya.kalsms; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class BootReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) + { + App app = App.getInstance(context.getApplicationContext()); + + app.setOutgoingMessageAlarm(); + + if (app.getLaunchOnBoot()) + { + Intent i = new Intent(context, Main.class); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + } + } +} diff --git a/src/org/envaya/kalsms/IncomingMessageForwarder.java b/src/org/envaya/kalsms/IncomingMessageForwarder.java index 31fdbe5..2b13b5e 100755 --- a/src/org/envaya/kalsms/IncomingMessageForwarder.java +++ b/src/org/envaya/kalsms/IncomingMessageForwarder.java @@ -17,7 +17,7 @@ public class IncomingMessageForwarder extends BroadcastReceiver { // source: http://www.devx.com/wireless/Article/39495/1954 public void onReceive(Context context, Intent intent) { try { - this.app = App.getInstance(context); + this.app = App.getInstance(context.getApplicationContext()); String action = intent.getAction(); diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java index 636bfc2..793b159 100755 --- a/src/org/envaya/kalsms/Main.java +++ b/src/org/envaya/kalsms/Main.java @@ -98,7 +98,7 @@ public void showLogMessage(String message) public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - this.app = App.getInstance(this.getApplication()); + this.app = App.getInstance(getApplicationContext()); setContentView(R.layout.main); PreferenceManager.setDefaultValues(this, R.xml.prefs, false); @@ -161,7 +161,7 @@ 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 + ")"); + item.setTitle("Retry Fwd (" + stuckMessages + ")"); return true; } diff --git a/src/org/envaya/kalsms/MessageStatusNotifier.java b/src/org/envaya/kalsms/MessageStatusNotifier.java index 669fc28..13c393e 100755 --- a/src/org/envaya/kalsms/MessageStatusNotifier.java +++ b/src/org/envaya/kalsms/MessageStatusNotifier.java @@ -13,7 +13,7 @@ public class MessageStatusNotifier extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - App app = App.getInstance(context); + App app = App.getInstance(context.getApplicationContext()); String id = intent.getExtras().getString("id"); diff --git a/src/org/envaya/kalsms/OutgoingMessagePoller.java b/src/org/envaya/kalsms/OutgoingMessagePoller.java index 6d56aff..92de238 100755 --- a/src/org/envaya/kalsms/OutgoingMessagePoller.java +++ b/src/org/envaya/kalsms/OutgoingMessagePoller.java @@ -10,7 +10,7 @@ public class OutgoingMessagePoller extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - app = App.getInstance(context); + app = App.getInstance(context.getApplicationContext()); app.checkOutgoingMessages(); app.retryStuckMessages(false); } diff --git a/src/org/envaya/kalsms/Prefs.java b/src/org/envaya/kalsms/Prefs.java index d09ee61..4c2c905 100755 --- a/src/org/envaya/kalsms/Prefs.java +++ b/src/org/envaya/kalsms/Prefs.java @@ -43,7 +43,7 @@ protected void onPause() { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - App app = App.getInstance(this.getApplication()); + App app = App.getInstance(this.getApplicationContext()); if (key.equals("outgoing_interval")) { app.setOutgoingMessageAlarm(); From e4cea3bae3ee3ce00aadb2fc427636c73ec9deed Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Tue, 13 Sep 2011 00:14:35 -0700 Subject: [PATCH 19/69] use alarm to schedule retries; fix bug where sms notfications were dropped when multiple messages were sent at once --- AndroidManifest.xml | 6 + src/org/envaya/kalsms/App.java | 156 +++++++++++------- .../kalsms/IncomingMessageForwarder.java | 2 - .../envaya/kalsms/IncomingMessageRetry.java | 16 ++ src/org/envaya/kalsms/Main.java | 2 +- .../envaya/kalsms/MessageStatusNotifier.java | 10 +- .../envaya/kalsms/OutgoingMessagePoller.java | 7 +- .../envaya/kalsms/OutgoingMessageRetry.java | 16 ++ 8 files changed, 149 insertions(+), 66 deletions(-) create mode 100755 src/org/envaya/kalsms/IncomingMessageRetry.java create mode 100755 src/org/envaya/kalsms/OutgoingMessageRetry.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 718ee60..4d98e82 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -35,6 +35,12 @@ + + + + + + diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index cf79b3a..98c1ef5 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -10,16 +10,14 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; 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.message.BasicNameValuePair; @@ -44,7 +42,7 @@ public class App { public Context context; public SharedPreferences settings; - private class QueuedMessage + private abstract class QueuedMessage { public T sms; public long nextAttemptTime = 0; @@ -52,46 +50,61 @@ private class QueuedMessage public boolean canAttemptNow() { - return (nextAttemptTime > 0 && nextAttemptTime < System.currentTimeMillis()); + return (nextAttemptTime > 0 && nextAttemptTime < SystemClock.elapsedRealtime()); } - + public boolean scheduleNextAttempt() { - long now = System.currentTimeMillis(); - numAttempts++; + long now = SystemClock.elapsedRealtime(); + numAttempts++; + + if (numAttempts > 4) + { + log("5th failure: giving up"); + return false; + } - int sec = 1000; + int second = 1000; + int minute = second * 60; if (numAttempts == 1) { log("1st failure; retry in 1 minute"); - nextAttemptTime = now + sec * 60; // 1 minute - return true; + nextAttemptTime = now + 1 * minute; } else if (numAttempts == 2) { log("2nd failure; retry in 10 minutes"); - nextAttemptTime = now + sec * 60 * 10; // 10 min - return true; + nextAttemptTime = now + 10 * minute; } else if (numAttempts == 3) { log("3rd failure; retry in 1 hour"); - nextAttemptTime = now + sec * 60 * 60; // 1 hour - return true; + nextAttemptTime = now + 60 * minute; } - 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; - } + log("4th failure: retry in 1 day"); + nextAttemptTime = now + 24 * 60 * minute; + } + + AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, + 0, + getAttemptIntent(), + 0); + + alarm.set( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + nextAttemptTime, + pendingIntent); + + return true; } + + public abstract void attemptNow(); + protected abstract Intent getAttemptIntent(); } private class QueuedIncomingSms extends QueuedMessage { @@ -99,15 +112,42 @@ public QueuedIncomingSms(SmsMessage sms) { this.sms = sms; } - } - + + public void attemptNow() + { + log("Retrying forwarding SMS from " + sms.getOriginatingAddress()); + trySendMessageToServer(sms); + } + + protected Intent getAttemptIntent() + { + Intent intent = new Intent(context, IncomingMessageRetry.class); + intent.setData(Uri.parse("kalsms://incoming/" + getSmsId(sms))); + return intent; + } + } + private class QueuedOutgoingSms extends QueuedMessage { public QueuedOutgoingSms(OutgoingSmsMessage sms) { this.sms = sms; + } + + public void attemptNow() + { + log("Retrying sending " +sms.getLogName() + " to " + sms.getTo()); + trySendSMS(sms); + } + + protected Intent getAttemptIntent() + { + Intent intent = new Intent(context, OutgoingMessageRetry.class); + intent.setData(Uri.parse("kalsms://outgoing/" + sms.getId())); + log("id=" + sms.getId()); + return intent; } } - + protected App(Context context) { this.context = context; @@ -134,7 +174,7 @@ public void log(String msg) Intent broadcast = new Intent(App.LOG_INTENT); broadcast.putExtra("message", msg); - context.sendBroadcast(broadcast); + context.sendBroadcast(broadcast); } public void checkOutgoingMessages() @@ -160,7 +200,7 @@ public void setOutgoingMessageAlarm() PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(context, OutgoingMessagePoller.class), - 0); + 0); alarm.cancel(pendingIntent); @@ -172,7 +212,7 @@ public void setOutgoingMessageAlarm() AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), pollSeconds * 1000, - pendingIntent); + pendingIntent); log("Checking for outgoing messages every " + pollSeconds + " sec"); } else @@ -282,10 +322,10 @@ else if (status.equals(App.STATUS_FAILED)) } } - public synchronized void retryStuckMessages(boolean retryAll) + public synchronized void retryStuckMessages() { - retryStuckOutgoingMessages(retryAll); - retryStuckIncomingMessages(retryAll); + retryStuckOutgoingMessages(); + retryStuckIncomingMessages(); } public synchronized int getStuckMessageCount() @@ -293,36 +333,19 @@ public synchronized int getStuckMessageCount() return outgoingSmsMap.size() + incomingSmsMap.size(); } - public synchronized void retryStuckOutgoingMessages(boolean retryAll) + public synchronized void retryStuckOutgoingMessages() { - for (Entry entry : outgoingSmsMap.entrySet()) + for (QueuedOutgoingSms queuedSms : outgoingSmsMap.values()) { - QueuedOutgoingSms queuedSms = entry.getValue(); - if (retryAll || queuedSms.canAttemptNow()) - { - queuedSms.nextAttemptTime = 0; - - log("Retrying sending " +queuedSms.sms.getLogName() - + " to " + queuedSms.sms.getTo()); - - trySendSMS(queuedSms.sms); - } + queuedSms.attemptNow(); } } - public synchronized void retryStuckIncomingMessages(boolean retryAll) + public synchronized void retryStuckIncomingMessages() { - for (Entry entry : incomingSmsMap.entrySet()) + for (QueuedIncomingSms queuedSms : incomingSmsMap.values()) { - QueuedIncomingSms queuedSms = entry.getValue(); - if (retryAll || queuedSms.canAttemptNow()) - { - queuedSms.nextAttemptTime = 0; - - log("Retrying forwarding SMS from " + queuedSms.sms.getOriginatingAddress()); - - trySendMessageToServer(queuedSms.sms); - } + queuedSms.attemptNow(); } } @@ -395,7 +418,7 @@ private void trySendSMS(OutgoingSmsMessage sms) SmsManager smgr = SmsManager.getDefault(); Intent intent = new Intent(context, MessageStatusNotifier.class); - intent.putExtra("id", sms.getId()); + intent.setData(Uri.parse("kalsms://outgoing/" + sms.getId())); PendingIntent sentIntent = PendingIntent.getBroadcast( this.context, @@ -503,4 +526,23 @@ private synchronized void notifyIncomingMessageStatus(SmsMessage sms, boolean su } } } + + public synchronized void retryIncomingMessage(String id) + { + QueuedIncomingSms queuedSms = incomingSmsMap.get(id); + if (queuedSms != null) + { + queuedSms.attemptNow(); + } + } + + public synchronized void retryOutgoingMessage(String id) + { + QueuedOutgoingSms queuedSms = outgoingSmsMap.get(id); + if (queuedSms != null) + { + queuedSms.attemptNow(); + } + } + } diff --git a/src/org/envaya/kalsms/IncomingMessageForwarder.java b/src/org/envaya/kalsms/IncomingMessageForwarder.java index 2b13b5e..54c19e9 100755 --- a/src/org/envaya/kalsms/IncomingMessageForwarder.java +++ b/src/org/envaya/kalsms/IncomingMessageForwarder.java @@ -5,9 +5,7 @@ import android.content.Intent; import android.os.Bundle; import android.telephony.SmsMessage; -import org.apache.http.HttpResponse; -import org.apache.http.message.BasicNameValuePair; public class IncomingMessageForwarder extends BroadcastReceiver { diff --git a/src/org/envaya/kalsms/IncomingMessageRetry.java b/src/org/envaya/kalsms/IncomingMessageRetry.java new file mode 100755 index 0000000..ac2f283 --- /dev/null +++ b/src/org/envaya/kalsms/IncomingMessageRetry.java @@ -0,0 +1,16 @@ + +package org.envaya.kalsms; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class IncomingMessageRetry extends BroadcastReceiver +{ + @Override + public void onReceive(Context context, Intent intent) + { + App app = App.getInstance(context.getApplicationContext()); + app.retryIncomingMessage(intent.getData().getLastPathSegment()); + } +} diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java index 793b159..0fb931f 100755 --- a/src/org/envaya/kalsms/Main.java +++ b/src/org/envaya/kalsms/Main.java @@ -131,7 +131,7 @@ public boolean onOptionsItemSelected(MenuItem item) { app.checkOutgoingMessages(); return true; case R.id.retry_now: - app.retryStuckMessages(true); + app.retryStuckMessages(); return true; case R.id.help: startActivity(new Intent(this, Help.class)); diff --git a/src/org/envaya/kalsms/MessageStatusNotifier.java b/src/org/envaya/kalsms/MessageStatusNotifier.java index 13c393e..bc83101 100755 --- a/src/org/envaya/kalsms/MessageStatusNotifier.java +++ b/src/org/envaya/kalsms/MessageStatusNotifier.java @@ -15,10 +15,18 @@ public class MessageStatusNotifier extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { App app = App.getInstance(context.getApplicationContext()); - String id = intent.getExtras().getString("id"); + String id = intent.getData().getLastPathSegment(); int resultCode = getResultCode(); + // uncomment to test retry on outgoing message failure + /* + if (Math.random() > 0.4) + { + resultCode = SmsManager.RESULT_ERROR_NO_SERVICE; + } + */ + app.notifyOutgoingMessageStatus(id, resultCode); } } diff --git a/src/org/envaya/kalsms/OutgoingMessagePoller.java b/src/org/envaya/kalsms/OutgoingMessagePoller.java index 92de238..97af50e 100755 --- a/src/org/envaya/kalsms/OutgoingMessagePoller.java +++ b/src/org/envaya/kalsms/OutgoingMessagePoller.java @@ -6,12 +6,9 @@ public class OutgoingMessagePoller extends BroadcastReceiver { - private App app; - @Override public void onReceive(Context context, Intent intent) { - app = App.getInstance(context.getApplicationContext()); - app.checkOutgoingMessages(); - app.retryStuckMessages(false); + App app = App.getInstance(context.getApplicationContext()); + app.checkOutgoingMessages(); } } diff --git a/src/org/envaya/kalsms/OutgoingMessageRetry.java b/src/org/envaya/kalsms/OutgoingMessageRetry.java new file mode 100755 index 0000000..8ed703b --- /dev/null +++ b/src/org/envaya/kalsms/OutgoingMessageRetry.java @@ -0,0 +1,16 @@ + +package org.envaya.kalsms; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class OutgoingMessageRetry extends BroadcastReceiver +{ + @Override + public void onReceive(Context context, Intent intent) + { + App app = App.getInstance(context.getApplicationContext()); + app.retryOutgoingMessage(intent.getData().getLastPathSegment()); + } +} From bead45360ff54c55b9a14126a93ffef0fc7d4681 Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Tue, 13 Sep 2011 11:31:01 -0700 Subject: [PATCH 20/69] set phone number and api version as regular post fields instead of custom http headers --- server/php/KalSMS.php | 4 ++-- src/org/envaya/kalsms/HttpTask.java | 25 ++++++++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/server/php/KalSMS.php b/server/php/KalSMS.php index 9e4ecee..5a68294 100755 --- a/server/php/KalSMS.php +++ b/server/php/KalSMS.php @@ -18,7 +18,7 @@ class KalSMS static function new_from_request() { - $version = @$_SERVER['HTTP_X_KALSMS_VERSION']; + $version = @$_POST['version']; return new KalSMS(); } @@ -56,7 +56,7 @@ private function _get_request_action() function get_request_phone_number() { - return @$_SERVER['HTTP_X_KALSMS_PHONENUMBER']; + return @$_POST['phone_number']; } function is_validated_request($correct_password) diff --git a/src/org/envaya/kalsms/HttpTask.java b/src/org/envaya/kalsms/HttpTask.java index 56c374b..7ae3797 100755 --- a/src/org/envaya/kalsms/HttpTask.java +++ b/src/org/envaya/kalsms/HttpTask.java @@ -10,6 +10,7 @@ import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.xml.parsers.DocumentBuilder; @@ -48,10 +49,10 @@ public HttpClient getHttpClient() return new DefaultHttpClient(httpParameters); } - private String getSignature(String url, BasicNameValuePair... params) + private String getSignature(String url, List params) { try { - Arrays.sort(params, new Comparator() { + Collections.sort(params, new Comparator() { public int compare(Object o1, Object o2) { return ((BasicNameValuePair)o1).getName().compareTo(((BasicNameValuePair)o2).getName()); @@ -97,13 +98,17 @@ protected HttpResponse doInBackground(BasicNameValuePair... params) { } HttpPost post = new HttpPost(url); + + List paramList + = new ArrayList(Arrays.asList(params)); + + paramList.add(new BasicNameValuePair("version", "2")); + paramList.add(new BasicNameValuePair("phone_number", app.getPhoneNumber())); - post.setEntity(new UrlEncodedFormEntity(Arrays.asList(params))); + post.setEntity(new UrlEncodedFormEntity(paramList)); - String signature = this.getSignature(url, params); + String signature = this.getSignature(url, paramList); - post.setHeader("X-Kalsms-Version", "2"); - post.setHeader("X-Kalsms-PhoneNumber", app.getPhoneNumber()); post.setHeader("X-Kalsms-Signature", signature); HttpClient client = getHttpClient(); @@ -126,11 +131,17 @@ else if (statusCode == 403) return null; } } - catch (Throwable ex) + catch (IOException ex) { app.logError("Error while contacting server", ex); return null; } + catch (Throwable ex) + { + app.logError("Unexpected error while contacting server", ex, true); + return null; + } + } protected String getDefaultToAddress() From e535266d9a1ea93a48a9cc90a38fa8d06a9003e2 Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Tue, 13 Sep 2011 12:46:37 -0700 Subject: [PATCH 21/69] instead of deleting messages from inbox, prevent them from getting there in the first place (unless user wants to keep them) --- AndroidManifest.xml | 2 +- res/xml/prefs.xml | 9 + src/org/envaya/kalsms/App.java | 531 ++++++++---------- src/org/envaya/kalsms/HttpTask.java | 56 +- .../kalsms/IncomingMessageForwarder.java | 35 +- 5 files changed, 270 insertions(+), 363 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4d98e82..2e41cf2 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -24,7 +24,7 @@ - + diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index 7ff56d5..478996c 100755 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -30,5 +30,14 @@ + + \ No newline at end of file diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index 98c1ef5..4e8afe2 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -7,9 +7,11 @@ import android.app.Activity; import android.app.AlarmManager; import android.app.PendingIntent; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.database.Cursor; import android.net.Uri; import android.os.SystemClock; import android.preference.PreferenceManager; @@ -22,349 +24,289 @@ import org.apache.http.message.BasicNameValuePair; public class App { - + public static final String ACTION_OUTGOING = "outgoing"; public static final String ACTION_INCOMING = "incoming"; public static final String ACTION_SEND_STATUS = "send_status"; - public static final String STATUS_QUEUED = "queued"; 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_INTENT = "org.envaya.kalsms.LOG"; - - private static App app; - + private static App app; private Map incomingSmsMap = new HashMap(); - private Map outgoingSmsMap = new HashMap(); - + private Map outgoingSmsMap = new HashMap(); public Context context; public SharedPreferences settings; - - private abstract class QueuedMessage - { + + private abstract class QueuedMessage { + public T sms; public long nextAttemptTime = 0; - public int numAttempts = 0; - - public boolean canAttemptNow() - { + public int numAttempts = 0; + + public boolean canAttemptNow() { return (nextAttemptTime > 0 && nextAttemptTime < SystemClock.elapsedRealtime()); } - public boolean scheduleNextAttempt() - { + public boolean scheduleNextAttempt() { long now = SystemClock.elapsedRealtime(); - numAttempts++; - - if (numAttempts > 4) - { + numAttempts++; + + if (numAttempts > 4) { log("5th failure: giving up"); return false; } - - int second = 1000; - int minute = second * 60; - - if (numAttempts == 1) - { + + int second = 1000; + int minute = second * 60; + + if (numAttempts == 1) { log("1st failure; retry in 1 minute"); - nextAttemptTime = now + 1 * minute; - } - else if (numAttempts == 2) - { + nextAttemptTime = now + 1 * minute; + } else if (numAttempts == 2) { log("2nd failure; retry in 10 minutes"); - nextAttemptTime = now + 10 * minute; - } - else if (numAttempts == 3) - { + nextAttemptTime = now + 10 * minute; + } else if (numAttempts == 3) { log("3rd failure; retry in 1 hour"); - nextAttemptTime = now + 60 * minute; - } - else - { + nextAttemptTime = now + 60 * minute; + } else { log("4th failure: retry in 1 day"); - nextAttemptTime = now + 24 * 60 * minute; - } - - AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - + nextAttemptTime = now + 24 * 60 * minute; + } + + AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, - 0, - getAttemptIntent(), - 0); + 0, + getAttemptIntent(), + 0); alarm.set( - AlarmManager.ELAPSED_REALTIME_WAKEUP, - nextAttemptTime, - pendingIntent); - - return true; + AlarmManager.ELAPSED_REALTIME_WAKEUP, + nextAttemptTime, + pendingIntent); + + return true; } - + public abstract void attemptNow(); - protected abstract Intent getAttemptIntent(); - } - + + protected abstract Intent getAttemptIntent(); + } + private class QueuedIncomingSms extends QueuedMessage { - public QueuedIncomingSms(SmsMessage sms) - { + + public QueuedIncomingSms(SmsMessage sms) { this.sms = sms; } - - public void attemptNow() - { - log("Retrying forwarding SMS from " + sms.getOriginatingAddress()); + + public void attemptNow() { + log("Retrying forwarding SMS from " + sms.getOriginatingAddress()); trySendMessageToServer(sms); } - - protected Intent getAttemptIntent() - { + + protected Intent getAttemptIntent() { Intent intent = new Intent(context, IncomingMessageRetry.class); intent.setData(Uri.parse("kalsms://incoming/" + getSmsId(sms))); return intent; } - } - + } + private class QueuedOutgoingSms extends QueuedMessage { - public QueuedOutgoingSms(OutgoingSmsMessage sms) - { + + public QueuedOutgoingSms(OutgoingSmsMessage sms) { this.sms = sms; } - public void attemptNow() - { - log("Retrying sending " +sms.getLogName() + " to " + sms.getTo()); - trySendSMS(sms); + public void attemptNow() { + log("Retrying sending " + sms.getLogName() + " to " + sms.getTo()); + trySendSMS(sms); } - - protected Intent getAttemptIntent() - { + + protected Intent getAttemptIntent() { Intent intent = new Intent(context, OutgoingMessageRetry.class); intent.setData(Uri.parse("kalsms://outgoing/" + sms.getId())); log("id=" + sms.getId()); return intent; - } - } - - protected App(Context context) - { + } + } + + protected App(Context context) { this.context = context; this.settings = PreferenceManager.getDefaultSharedPreferences(context); } - - public static App getInstance(Context context) - { - if (app == null) - { + + public static App getInstance(Context context) { + if (app == null) { app = new App(context); } return app; } - - public void debug(String msg) - { - Log.d(LOG_NAME, msg); + + public void debug(String msg) { + Log.d(LOG_NAME, msg); } - - public void log(String msg) - { + + public void log(String msg) { Log.d(LOG_NAME, msg); - + Intent broadcast = new Intent(App.LOG_INTENT); broadcast.putExtra("message", msg); context.sendBroadcast(broadcast); } - public void checkOutgoingMessages() - { + public void checkOutgoingMessages() { String serverUrl = getServerUrl(); - if (serverUrl.length() > 0) - { + if (serverUrl.length() > 0) { log("Checking for outgoing messages"); - new PollerTask().execute( - new BasicNameValuePair("action", App.ACTION_OUTGOING) - ); - } - else - { + new PollerTask().execute( + new BasicNameValuePair("action", App.ACTION_OUTGOING)); + } else { log("Can't check outgoing messages; server URL not set"); } } - - public void setOutgoingMessageAlarm() - { - AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - + + public void setOutgoingMessageAlarm() { + AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, - new Intent(context, OutgoingMessagePoller.class), + new Intent(context, OutgoingMessagePoller.class), 0); - - alarm.cancel(pendingIntent); - + + alarm.cancel(pendingIntent); + int pollSeconds = getOutgoingPollSeconds(); - - if (pollSeconds > 0) - { + + if (pollSeconds > 0) { alarm.setRepeating( - AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime(), - pollSeconds * 1000, - pendingIntent); - log("Checking for outgoing messages every " + pollSeconds + " sec"); - } - else - { + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime(), + pollSeconds * 1000, + pendingIntent); + log("Checking for outgoing messages every " + pollSeconds + " sec"); + } else { log("Not checking for outgoing messages."); } } - - public void logError(Throwable ex) - { + + public void logError(Throwable ex) { logError("ERROR", ex); - } - - public void logError(String msg, Throwable ex) - { + } + + public void logError(String msg, Throwable ex) { logError(msg, ex, false); } - - public void logError(String msg, Throwable ex, boolean detail) - { + + public void logError(String msg, Throwable ex, boolean detail) { log(msg + ": " + ex.getClass().getName() + ": " + ex.getMessage()); - - if (detail) - { - for (StackTraceElement elem : ex.getStackTrace()) - { + + if (detail) { + for (StackTraceElement elem : ex.getStackTrace()) { log(elem.getClassName() + ":" + elem.getMethodName() + ":" + elem.getLineNumber()); } Throwable innerEx = ex.getCause(); - if (innerEx != null) - { + if (innerEx != null) { logError("Inner exception:", innerEx, true); } } - } - - public String getDisplayString(String str) - { - if (str.length() == 0) - { + } + + public String getDisplayString(String str) { + if (str.length() == 0) { return "(not set)"; - } - else - { + } else { return str; - } + } } - - public String getServerUrl() - { + + public String getServerUrl() { return settings.getString("server_url", ""); } - public String getPhoneNumber() - { + public String getPhoneNumber() { return settings.getString("phone_number", ""); } + + public int getOutgoingPollSeconds() { + return Integer.parseInt(settings.getString("outgoing_interval", "0")); + } + + public boolean getLaunchOnBoot() { + return settings.getBoolean("launch_on_boot", false); + } - public int getOutgoingPollSeconds() - { - return Integer.parseInt(settings.getString("outgoing_interval", "0")); - } - - public boolean getLaunchOnBoot() - { - return settings.getBoolean("launch_on_boot", true); - } - - public String getPassword() + public boolean getKeepInInbox() { + return settings.getBoolean("keep_in_inbox", false); + } + + public String getPassword() { return settings.getString("password", ""); - } - - private void notifyStatus(OutgoingSmsMessage sms, String status, String errorMessage) - { + } + + private void notifyStatus(OutgoingSmsMessage sms, String status, String errorMessage) { String serverId = sms.getServerId(); - + String logMessage; - if (status.equals(App.STATUS_SENT)) - { - logMessage = "sent successfully"; - } - else if (status.equals(App.STATUS_FAILED)) - { + if (status.equals(App.STATUS_SENT)) { + logMessage = "sent successfully"; + } else if (status.equals(App.STATUS_FAILED)) { logMessage = "could not be sent (" + errorMessage + ")"; - } - else - { + } else { logMessage = "queued"; } String smsDesc = sms.getLogName(); - - if (serverId != null) - { - app.log("Notifying server " + smsDesc + " " + logMessage); + + 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 - { + 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() - { + + public synchronized void retryStuckMessages() { retryStuckOutgoingMessages(); retryStuckIncomingMessages(); } - - public synchronized int getStuckMessageCount() - { + + public synchronized int getStuckMessageCount() { return outgoingSmsMap.size() + incomingSmsMap.size(); } - - public synchronized void retryStuckOutgoingMessages() - { - for (QueuedOutgoingSms queuedSms : outgoingSmsMap.values()) - { + + public synchronized void retryStuckOutgoingMessages() { + for (QueuedOutgoingSms queuedSms : outgoingSmsMap.values()) { queuedSms.attemptNow(); } } - - public synchronized void retryStuckIncomingMessages() - { - for (QueuedIncomingSms queuedSms : incomingSmsMap.values()) - { + + public synchronized void retryStuckIncomingMessages() { + for (QueuedIncomingSms queuedSms : incomingSmsMap.values()) { queuedSms.attemptNow(); - } + } } - - public synchronized void notifyOutgoingMessageStatus(String id, int resultCode) - { + + public synchronized void notifyOutgoingMessageStatus(String id, int resultCode) { QueuedOutgoingSms queuedSms = outgoingSmsMap.get(id); - - if (queuedSms == null) - { + + if (queuedSms == null) { return; } - - OutgoingSmsMessage sms = queuedSms.sms; - + + OutgoingSmsMessage sms = queuedSms.sms; + switch (resultCode) { case Activity.RESULT_OK: this.notifyStatus(sms, App.STATUS_SENT, ""); break; - case SmsManager.RESULT_ERROR_GENERIC_FAILURE: + case SmsManager.RESULT_ERROR_GENERIC_FAILURE: this.notifyStatus(sms, App.STATUS_FAILED, "generic failure"); break; case SmsManager.RESULT_ERROR_RADIO_OFF: @@ -373,7 +315,7 @@ public synchronized void notifyOutgoingMessageStatus(String id, int resultCode) case SmsManager.RESULT_ERROR_NO_SERVICE: this.notifyStatus(sms, App.STATUS_FAILED, "no service"); break; - case SmsManager.RESULT_ERROR_NULL_PDU: + case SmsManager.RESULT_ERROR_NULL_PDU: this.notifyStatus(sms, App.STATUS_FAILED, "null PDU"); break; default: @@ -381,69 +323,63 @@ public synchronized void notifyOutgoingMessageStatus(String id, int resultCode) break; } - switch (resultCode) { - case SmsManager.RESULT_ERROR_GENERIC_FAILURE: + switch (resultCode) { + case SmsManager.RESULT_ERROR_GENERIC_FAILURE: case SmsManager.RESULT_ERROR_RADIO_OFF: case SmsManager.RESULT_ERROR_NO_SERVICE: - if (!queuedSms.scheduleNextAttempt()) - { + if (!queuedSms.scheduleNextAttempt()) { outgoingSmsMap.remove(id); } break; default: outgoingSmsMap.remove(id); break; - } - + } + } - - public synchronized void sendSMS(OutgoingSmsMessage sms) - { + + public synchronized void sendSMS(OutgoingSmsMessage sms) { String id = sms.getId(); - if (outgoingSmsMap.containsKey(id)) - { + 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()); + + log("Sending " + sms.getLogName() + " to " + sms.getTo()); trySendSMS(sms); } - - private void trySendSMS(OutgoingSmsMessage sms) - { + + private void trySendSMS(OutgoingSmsMessage sms) { SmsManager smgr = SmsManager.getDefault(); - + Intent intent = new Intent(context, MessageStatusNotifier.class); intent.setData(Uri.parse("kalsms://outgoing/" + sms.getId())); - + PendingIntent sentIntent = PendingIntent.getBroadcast( this.context, 0, intent, PendingIntent.FLAG_ONE_SHOT); - - smgr.sendTextMessage(sms.getTo(), null, sms.getMessage(), sentIntent, null); + + smgr.sendTextMessage(sms.getTo(), null, sms.getMessage(), sentIntent, null); } - + private class PollerTask extends HttpTask { - public PollerTask() - { + public PollerTask() { super(app); } - + @Override protected void handleResponse(HttpResponse response) throws Exception { for (OutgoingSmsMessage reply : parseResponseXML(response)) { app.sendSMS(reply); - } - } + } + } } - private class ForwarderTask extends HttpTask { @@ -455,94 +391,79 @@ public ForwarderTask(SmsMessage originalSms) { } @Override - protected String getDefaultToAddress() - { + 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); + } + + app.notifyIncomingMessageStatus(originalSms, true); } - + @Override - protected void handleFailure() - { + protected void handleFailure() { app.notifyIncomingMessageStatus(originalSms, false); - } - } - - private String getSmsId(SmsMessage sms) - { + } + } + + private String getSmsId(SmsMessage sms) { return sms.getOriginatingAddress() + ":" + sms.getMessageBody() + ":" + sms.getTimestampMillis(); } - - public synchronized void sendMessageToServer(SmsMessage sms) - { + + public synchronized void sendMessageToServer(SmsMessage sms) { String id = getSmsId(sms); - if (incomingSmsMap.containsKey(id)) - { + if (incomingSmsMap.containsKey(id)) { log("Duplicate incoming SMS, skipping"); return; - } - - QueuedIncomingSms queuedSms = new QueuedIncomingSms(sms); + } + + QueuedIncomingSms queuedSms = new QueuedIncomingSms(sms); incomingSmsMap.put(id, queuedSms); - + app.log("Received SMS from " + sms.getOriginatingAddress()); - + trySendMessageToServer(sms); - } - - public void trySendMessageToServer(SmsMessage 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) - ); + new BasicNameValuePair("from", sender), + new BasicNameValuePair("message", message), + new BasicNameValuePair("action", App.ACTION_INCOMING)); } - - private synchronized void notifyIncomingMessageStatus(SmsMessage sms, boolean success) - { + + private synchronized void notifyIncomingMessageStatus(SmsMessage sms, boolean success) { String id = getSmsId(sms); QueuedIncomingSms queuedSms = incomingSmsMap.get(id); - - if (queuedSms != null) - { - if (success || !queuedSms.scheduleNextAttempt()) - { + + if (queuedSms != null) { + if (success || !queuedSms.scheduleNextAttempt()) { incomingSmsMap.remove(id); - } + } } } - - public synchronized void retryIncomingMessage(String id) - { + + public synchronized void retryIncomingMessage(String id) { QueuedIncomingSms queuedSms = incomingSmsMap.get(id); - if (queuedSms != null) - { + if (queuedSms != null) { queuedSms.attemptNow(); - } + } } - - public synchronized void retryOutgoingMessage(String id) - { + + public synchronized void retryOutgoingMessage(String id) { QueuedOutgoingSms queuedSms = outgoingSmsMap.get(id); - if (queuedSms != null) - { + if (queuedSms != null) { queuedSms.attemptNow(); - } + } } - } diff --git a/src/org/envaya/kalsms/HttpTask.java b/src/org/envaya/kalsms/HttpTask.java index 7ae3797..938a581 100755 --- a/src/org/envaya/kalsms/HttpTask.java +++ b/src/org/envaya/kalsms/HttpTask.java @@ -7,7 +7,9 @@ import android.os.AsyncTask; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -50,41 +52,35 @@ public HttpClient getHttpClient() } private String getSignature(String url, List params) + throws NoSuchAlgorithmException, UnsupportedEncodingException { - try { - Collections.sort(params, new Comparator() { - public int compare(Object o1, Object o2) - { - return ((BasicNameValuePair)o1).getName().compareTo(((BasicNameValuePair)o2).getName()); - } - }); - - StringBuilder builder = new StringBuilder(); - builder.append(url); - for (BasicNameValuePair param : params) + Collections.sort(params, new Comparator() { + public int compare(Object o1, Object o2) { - builder.append(","); - builder.append(param.getName()); - builder.append("="); - builder.append(param.getValue()); + return ((BasicNameValuePair)o1).getName().compareTo(((BasicNameValuePair)o2).getName()); } + }); + + StringBuilder builder = new StringBuilder(); + builder.append(url); + for (BasicNameValuePair param : params) + { builder.append(","); - builder.append(app.getPassword()); - - String value = builder.toString(); - - MessageDigest md = MessageDigest.getInstance("SHA-1"); - - md.update(value.getBytes("utf-8")); - - byte[] digest = md.digest(); - - return new String(Base64Coder.encode(digest)); - - } catch (Exception ex) { - app.logError("Error computing signature", ex); + builder.append(param.getName()); + builder.append("="); + builder.append(param.getValue()); } - return ""; + builder.append(","); + builder.append(app.getPassword()); + + String value = builder.toString(); + + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(value.getBytes("utf-8")); + + byte[] digest = md.digest(); + + return new String(Base64Coder.encode(digest)); } protected HttpResponse doInBackground(BasicNameValuePair... params) { diff --git a/src/org/envaya/kalsms/IncomingMessageForwarder.java b/src/org/envaya/kalsms/IncomingMessageForwarder.java index 54c19e9..adf80d6 100755 --- a/src/org/envaya/kalsms/IncomingMessageForwarder.java +++ b/src/org/envaya/kalsms/IncomingMessageForwarder.java @@ -13,18 +13,21 @@ public class IncomingMessageForwarder extends BroadcastReceiver { @Override // source: http://www.devx.com/wireless/Article/39495/1954 - public void onReceive(Context context, Intent intent) { + public void onReceive(Context context, Intent intent) { + app = App.getInstance(context.getApplicationContext()); + try { - this.app = App.getInstance(context.getApplicationContext()); - String action = intent.getAction(); if (action.equals("android.provider.Telephony.SMS_RECEIVED")) { for (SmsMessage sms : getMessagesFromIntent(intent)) { app.sendMessageToServer(sms); - - //DeleteSMSFromInbox(context, mesg); + } + + if (!app.getKeepInInbox()) + { + this.abortBroadcast(); } } } catch (Throwable ex) { @@ -32,28 +35,6 @@ public void onReceive(Context context, Intent intent) { } } - /* - private void DeleteSMSFromInbox(Context context, SmsMessage mesg) { - Log.d("KALSMS", "try to delete SMS"); - try { - Uri uriSms = Uri.parse("content://sms/inbox"); - StringBuilder sb = new StringBuilder(); - sb.append("address='" + mesg.getOriginatingAddress() + "' AND "); - sb.append("body='" + mesg.getMessageBody() + "'"); - Cursor c = context.getContentResolver().query(uriSms, null, sb.toString(), null, null); - c.moveToFirst(); - int thread_id = c.getInt(1); - context.getContentResolver().delete(Uri.parse("content://sms/conversations/" + thread_id), null, null); - c.close(); - } catch (Exception ex) { - // deletions don't work most of the time since the timing of the - // receipt and saving to the inbox - // makes it difficult to match up perfectly. the SMS might not be in - // the inbox yet when this receiver triggers! - Log.d("SmsReceiver", "Error deleting sms from inbox: " + ex.getMessage()); - } - } - */ // from http://github.com/dimagi/rapidandroid // source: http://www.devx.com/wireless/Article/39495/1954 private SmsMessage[] getMessagesFromIntent(Intent intent) { From e79fe7ce23a2afa339d25cb94674c72bf4ea98ef Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Tue, 13 Sep 2011 14:55:26 -0700 Subject: [PATCH 22/69] refactoring --- src/org/envaya/kalsms/App.java | 328 ++++-------------- src/org/envaya/kalsms/ForwarderTask.java | 36 ++ src/org/envaya/kalsms/HttpTask.java | 42 +-- src/org/envaya/kalsms/IncomingMessage.java | 50 +++ .../kalsms/IncomingMessageForwarder.java | 25 +- src/org/envaya/kalsms/Main.java | 9 +- src/org/envaya/kalsms/OutgoingMessage.java | 107 ++++++ src/org/envaya/kalsms/OutgoingSmsMessage.java | 75 ---- src/org/envaya/kalsms/PollerTask.java | 19 + src/org/envaya/kalsms/QueuedMessage.java | 69 ++++ 10 files changed, 395 insertions(+), 365 deletions(-) create mode 100755 src/org/envaya/kalsms/ForwarderTask.java create mode 100755 src/org/envaya/kalsms/IncomingMessage.java create mode 100755 src/org/envaya/kalsms/OutgoingMessage.java delete mode 100755 src/org/envaya/kalsms/OutgoingSmsMessage.java create mode 100755 src/org/envaya/kalsms/PollerTask.java create mode 100755 src/org/envaya/kalsms/QueuedMessage.java diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index 4e8afe2..5d4f9e6 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -1,26 +1,17 @@ -/* - * To change this template, choose Tools | Templates - * and open the template in the editor. - */ package org.envaya.kalsms; import android.app.Activity; import android.app.AlarmManager; import android.app.PendingIntent; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; 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.Map; -import org.apache.http.HttpResponse; import org.apache.http.message.BasicNameValuePair; public class App { @@ -34,104 +25,13 @@ public class App { public static final String LOG_NAME = "KALSMS"; public static final String LOG_INTENT = "org.envaya.kalsms.LOG"; private static App app; - private Map incomingSmsMap = new HashMap(); - private Map outgoingSmsMap = new HashMap(); + + private Map incomingSmsMap = new HashMap(); + private Map outgoingSmsMap = new HashMap(); + public Context context; public SharedPreferences settings; - private abstract class QueuedMessage { - - public T sms; - public long nextAttemptTime = 0; - public int numAttempts = 0; - - public boolean canAttemptNow() { - return (nextAttemptTime > 0 && nextAttemptTime < SystemClock.elapsedRealtime()); - } - - public boolean scheduleNextAttempt() { - long now = SystemClock.elapsedRealtime(); - numAttempts++; - - if (numAttempts > 4) { - log("5th failure: giving up"); - return false; - } - - int second = 1000; - int minute = second * 60; - - if (numAttempts == 1) { - log("1st failure; retry in 1 minute"); - nextAttemptTime = now + 1 * minute; - } else if (numAttempts == 2) { - log("2nd failure; retry in 10 minutes"); - nextAttemptTime = now + 10 * minute; - } else if (numAttempts == 3) { - log("3rd failure; retry in 1 hour"); - nextAttemptTime = now + 60 * minute; - } else { - log("4th failure: retry in 1 day"); - nextAttemptTime = now + 24 * 60 * minute; - } - - AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, - 0, - getAttemptIntent(), - 0); - - alarm.set( - AlarmManager.ELAPSED_REALTIME_WAKEUP, - nextAttemptTime, - pendingIntent); - - return true; - } - - public abstract void attemptNow(); - - protected abstract Intent getAttemptIntent(); - } - - private class QueuedIncomingSms extends QueuedMessage { - - public QueuedIncomingSms(SmsMessage sms) { - this.sms = sms; - } - - public void attemptNow() { - log("Retrying forwarding SMS from " + sms.getOriginatingAddress()); - trySendMessageToServer(sms); - } - - protected Intent getAttemptIntent() { - Intent intent = new Intent(context, IncomingMessageRetry.class); - intent.setData(Uri.parse("kalsms://incoming/" + getSmsId(sms))); - return intent; - } - } - - private class QueuedOutgoingSms extends QueuedMessage { - - public QueuedOutgoingSms(OutgoingSmsMessage sms) { - this.sms = sms; - } - - public void attemptNow() { - log("Retrying sending " + sms.getLogName() + " to " + sms.getTo()); - trySendSMS(sms); - } - - protected Intent getAttemptIntent() { - Intent intent = new Intent(context, OutgoingMessageRetry.class); - intent.setData(Uri.parse("kalsms://outgoing/" + sms.getId())); - log("id=" + sms.getId()); - return intent; - } - } - protected App(Context context) { this.context = context; this.settings = PreferenceManager.getDefaultSharedPreferences(context); @@ -144,24 +44,12 @@ public static App getInstance(Context context) { return app; } - public void debug(String msg) { - Log.d(LOG_NAME, msg); - } - - public void log(String msg) { - Log.d(LOG_NAME, msg); - - Intent broadcast = new Intent(App.LOG_INTENT); - broadcast.putExtra("message", msg); - context.sendBroadcast(broadcast); - } - - public void checkOutgoingMessages() { + public void checkOutgoingMessages() + { String serverUrl = getServerUrl(); if (serverUrl.length() > 0) { log("Checking for outgoing messages"); - new PollerTask().execute( - new BasicNameValuePair("action", App.ACTION_OUTGOING)); + new PollerTask(this).execute(); } else { log("Can't check outgoing messages; server URL not set"); } @@ -191,28 +79,6 @@ public void setOutgoingMessageAlarm() { } } - public void logError(Throwable ex) { - logError("ERROR", ex); - } - - public void logError(String msg, Throwable ex) { - logError(msg, ex, false); - } - - public void logError(String msg, Throwable ex, boolean detail) { - log(msg + ": " + ex.getClass().getName() + ": " + ex.getMessage()); - - if (detail) { - for (StackTraceElement elem : ex.getStackTrace()) { - log(elem.getClassName() + ":" + elem.getMethodName() + ":" + elem.getLineNumber()); - } - Throwable innerEx = ex.getCause(); - if (innerEx != null) { - logError("Inner exception:", innerEx, true); - } - } - } - public String getDisplayString(String str) { if (str.length() == 0) { return "(not set)"; @@ -246,7 +112,7 @@ public String getPassword() { return settings.getString("password", ""); } - private void notifyStatus(OutgoingSmsMessage sms, String status, String errorMessage) { + private void notifyStatus(OutgoingMessage sms, String status, String errorMessage) { String serverId = sms.getServerId(); String logMessage; @@ -262,11 +128,12 @@ private void notifyStatus(OutgoingSmsMessage sms, String status, String errorMes 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)); + new HttpTask(app, + new BasicNameValuePair("id", serverId), + new BasicNameValuePair("status", status), + new BasicNameValuePair("error", errorMessage), + new BasicNameValuePair("action", App.ACTION_SEND_STATUS) + ).execute(); } else { app.log(smsDesc + " " + logMessage); } @@ -282,26 +149,36 @@ public synchronized int getStuckMessageCount() { } public synchronized void retryStuckOutgoingMessages() { - for (QueuedOutgoingSms queuedSms : outgoingSmsMap.values()) { - queuedSms.attemptNow(); + for (OutgoingMessage sms : outgoingSmsMap.values()) { + sms.retryNow(); } } public synchronized void retryStuckIncomingMessages() { - for (QueuedIncomingSms queuedSms : incomingSmsMap.values()) { - queuedSms.attemptNow(); + for (IncomingMessage sms : incomingSmsMap.values()) { + sms.retryNow(); } } + + public synchronized void setIncomingMessageStatus(IncomingMessage sms, boolean success) { + String id = sms.getId(); + if (success) + { + incomingSmsMap.remove(id); + } + else if (!sms.scheduleRetry()) + { + incomingSmsMap.remove(id); + } + } public synchronized void notifyOutgoingMessageStatus(String id, int resultCode) { - QueuedOutgoingSms queuedSms = outgoingSmsMap.get(id); + OutgoingMessage sms = outgoingSmsMap.get(id); - if (queuedSms == null) { + if (sms == null) { return; } - OutgoingSmsMessage sms = queuedSms.sms; - switch (resultCode) { case Activity.RESULT_OK: this.notifyStatus(sms, App.STATUS_SENT, ""); @@ -327,7 +204,7 @@ public synchronized void notifyOutgoingMessageStatus(String id, int resultCode) case SmsManager.RESULT_ERROR_GENERIC_FAILURE: case SmsManager.RESULT_ERROR_RADIO_OFF: case SmsManager.RESULT_ERROR_NO_SERVICE: - if (!queuedSms.scheduleNextAttempt()) { + if (!sms.scheduleRetry()) { outgoingSmsMap.remove(id); } break; @@ -335,135 +212,82 @@ public synchronized void notifyOutgoingMessageStatus(String id, int resultCode) outgoingSmsMap.remove(id); break; } - } - public synchronized void sendSMS(OutgoingSmsMessage sms) { + public synchronized void sendOutgoingMessage(OutgoingMessage 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); + outgoingSmsMap.put(id, sms); log("Sending " + sms.getLogName() + " to " + sms.getTo()); - trySendSMS(sms); + sms.trySend(); } - private void trySendSMS(OutgoingSmsMessage sms) { - SmsManager smgr = SmsManager.getDefault(); + public synchronized void forwardToServer(IncomingMessage sms) { + String id = sms.getId(); + + if (incomingSmsMap.containsKey(id)) { + log("Duplicate incoming SMS, skipping"); + return; + } - Intent intent = new Intent(context, MessageStatusNotifier.class); - intent.setData(Uri.parse("kalsms://outgoing/" + sms.getId())); + incomingSmsMap.put(id, sms); - PendingIntent sentIntent = PendingIntent.getBroadcast( - this.context, - 0, - intent, - PendingIntent.FLAG_ONE_SHOT); + app.log("Received SMS from " + sms.getFrom()); - smgr.sendTextMessage(sms.getTo(), null, sms.getMessage(), sentIntent, null); + sms.tryForwardToServer(); } - private class PollerTask extends HttpTask { - - public PollerTask() { - super(app); - } - - @Override - protected void handleResponse(HttpResponse response) throws Exception { - for (OutgoingSmsMessage reply : parseResponseXML(response)) { - app.sendSMS(reply); - } + public synchronized void retryIncomingMessage(String id) { + IncomingMessage sms = incomingSmsMap.get(id); + if (sms != null) { + sms.retryNow(); } } - 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); + public synchronized void retryOutgoingMessage(String id) { + OutgoingMessage sms = outgoingSmsMap.get(id); + if (sms != null) { + sms.retryNow(); } } - private String getSmsId(SmsMessage sms) { - return sms.getOriginatingAddress() + ":" + sms.getMessageBody() + ":" + sms.getTimestampMillis(); + public void debug(String msg) { + Log.d(LOG_NAME, msg); } - 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()); + public void log(String msg) { + Log.d(LOG_NAME, msg); - trySendMessageToServer(sms); + Intent broadcast = new Intent(App.LOG_INTENT); + broadcast.putExtra("message", msg); + context.sendBroadcast(broadcast); } - - 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)); - + + public void logError(Throwable ex) { + logError("ERROR", ex); } - private synchronized void notifyIncomingMessageStatus(SmsMessage sms, boolean success) { - String id = getSmsId(sms); + public void logError(String msg, Throwable ex) { + logError(msg, ex, false); + } - QueuedIncomingSms queuedSms = incomingSmsMap.get(id); + public void logError(String msg, Throwable ex, boolean detail) { + log(msg + ": " + ex.getClass().getName() + ": " + ex.getMessage()); - if (queuedSms != null) { - if (success || !queuedSms.scheduleNextAttempt()) { - incomingSmsMap.remove(id); + if (detail) { + for (StackTraceElement elem : ex.getStackTrace()) { + log(elem.getClassName() + ":" + elem.getMethodName() + ":" + elem.getLineNumber()); + } + Throwable innerEx = ex.getCause(); + if (innerEx != null) { + logError("Inner exception:", innerEx, true); } } } - public synchronized void retryIncomingMessage(String id) { - QueuedIncomingSms queuedSms = incomingSmsMap.get(id); - if (queuedSms != null) { - queuedSms.attemptNow(); - } - } - - public synchronized void retryOutgoingMessage(String id) { - QueuedOutgoingSms queuedSms = outgoingSmsMap.get(id); - if (queuedSms != null) { - queuedSms.attemptNow(); - } - } } diff --git a/src/org/envaya/kalsms/ForwarderTask.java b/src/org/envaya/kalsms/ForwarderTask.java new file mode 100755 index 0000000..6439934 --- /dev/null +++ b/src/org/envaya/kalsms/ForwarderTask.java @@ -0,0 +1,36 @@ +package org.envaya.kalsms; + +import org.apache.http.HttpResponse; +import org.apache.http.message.BasicNameValuePair; + +public class ForwarderTask extends HttpTask { + + private IncomingMessage originalSms; + + public ForwarderTask(IncomingMessage originalSms, BasicNameValuePair... paramsArr) { + super(originalSms.app, paramsArr); + this.originalSms = originalSms; + + params.add(new BasicNameValuePair("action", App.ACTION_INCOMING)); + } + + @Override + protected String getDefaultToAddress() { + return originalSms.getFrom(); + } + + @Override + protected void handleResponse(HttpResponse response) throws Exception { + + for (OutgoingMessage reply : parseResponseXML(response)) { + app.sendOutgoingMessage(reply); + } + + app.setIncomingMessageStatus(originalSms, true); + } + + @Override + protected void handleFailure() { + app.setIncomingMessageStatus(originalSms, false); + } +} diff --git a/src/org/envaya/kalsms/HttpTask.java b/src/org/envaya/kalsms/HttpTask.java index 938a581..c8bc9df 100755 --- a/src/org/envaya/kalsms/HttpTask.java +++ b/src/org/envaya/kalsms/HttpTask.java @@ -33,14 +33,21 @@ import org.w3c.dom.NodeList; import org.xml.sax.SAXException; -public class HttpTask extends AsyncTask { +public class HttpTask extends AsyncTask { - private App app; + protected App app; - public HttpTask(App app) + protected String url; + protected List params = new ArrayList(); + + public HttpTask(App app, BasicNameValuePair... paramsArr) { super(); - this.app = app; + this.app = app; + this.url = app.getServerUrl(); + params = new ArrayList(Arrays.asList(paramsArr)); + params.add(new BasicNameValuePair("version", "2")); + params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber())); } public HttpClient getHttpClient() @@ -51,7 +58,7 @@ public HttpClient getHttpClient() return new DefaultHttpClient(httpParameters); } - private String getSignature(String url, List params) + private String getSignature() throws NoSuchAlgorithmException, UnsupportedEncodingException { Collections.sort(params, new Comparator() { @@ -83,27 +90,19 @@ public int compare(Object o1, Object o2) return new String(Base64Coder.encode(digest)); } - protected HttpResponse doInBackground(BasicNameValuePair... params) { + protected HttpResponse doInBackground(String... ignored) { 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); - - List paramList - = new ArrayList(Arrays.asList(params)); - - paramList.add(new BasicNameValuePair("version", "2")); - paramList.add(new BasicNameValuePair("phone_number", app.getPhoneNumber())); - - post.setEntity(new UrlEncodedFormEntity(paramList)); + + post.setEntity(new UrlEncodedFormEntity(params)); - String signature = this.getSignature(url, paramList); + String signature = getSignature(); post.setHeader("X-Kalsms-Signature", signature); @@ -145,10 +144,10 @@ protected String getDefaultToAddress() return ""; } - protected List parseResponseXML(HttpResponse response) + protected List parseResponseXML(HttpResponse response) throws IOException, ParserConfigurationException, SAXException { - List messages = new ArrayList(); + List messages = new ArrayList(); InputStream responseStream = response.getEntity().getContent(); DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document xml = xmlBuilder.parse(responseStream); @@ -156,7 +155,8 @@ protected List parseResponseXML(HttpResponse response) NodeList smsNodes = xml.getElementsByTagName("Sms"); for (int i = 0; i < smsNodes.getLength(); i++) { Element smsElement = (Element) smsNodes.item(i); - OutgoingSmsMessage sms = new OutgoingSmsMessage(); + + OutgoingMessage sms = new OutgoingMessage(app); sms.setFrom(app.getPhoneNumber()); @@ -169,7 +169,7 @@ protected List parseResponseXML(HttpResponse response) sms.setServerId(serverId.equals("") ? null : serverId); Node firstChild = smsElement.getFirstChild(); - sms.setMessage(firstChild != null ? firstChild.getNodeValue(): ""); + sms.setMessageBody(firstChild != null ? firstChild.getNodeValue(): ""); messages.add(sms); } diff --git a/src/org/envaya/kalsms/IncomingMessage.java b/src/org/envaya/kalsms/IncomingMessage.java new file mode 100755 index 0000000..73a09d4 --- /dev/null +++ b/src/org/envaya/kalsms/IncomingMessage.java @@ -0,0 +1,50 @@ +package org.envaya.kalsms; + +import android.content.Intent; +import android.net.Uri; +import android.telephony.SmsMessage; +import org.apache.http.message.BasicNameValuePair; + +public class IncomingMessage extends QueuedMessage { + + public SmsMessage sms; + + public IncomingMessage(App app, SmsMessage sms) { + super(app); + this.sms = sms; + } + + public String getMessageBody() + { + return sms.getMessageBody(); + } + + public String getFrom() + { + return sms.getOriginatingAddress(); + } + + public String getId() + { + return sms.getOriginatingAddress() + ":" + sms.getMessageBody() + ":" + sms.getTimestampMillis(); + } + + public void retryNow() { + app.log("Retrying forwarding SMS from " + sms.getOriginatingAddress()); + tryForwardToServer(); + } + + public void tryForwardToServer() { + new ForwarderTask(this, + new BasicNameValuePair("from", getFrom()), + new BasicNameValuePair("message", getMessageBody()) + ).execute(); + } + + + protected Intent getRetryIntent() { + Intent intent = new Intent(app.context, IncomingMessageRetry.class); + intent.setData(Uri.parse("kalsms://incoming/" + this.getId())); + return intent; + } +} diff --git a/src/org/envaya/kalsms/IncomingMessageForwarder.java b/src/org/envaya/kalsms/IncomingMessageForwarder.java index adf80d6..3c4d43d 100755 --- a/src/org/envaya/kalsms/IncomingMessageForwarder.java +++ b/src/org/envaya/kalsms/IncomingMessageForwarder.java @@ -5,6 +5,8 @@ import android.content.Intent; import android.os.Bundle; import android.telephony.SmsMessage; +import java.util.ArrayList; +import java.util.List; public class IncomingMessageForwarder extends BroadcastReceiver { @@ -21,8 +23,8 @@ public void onReceive(Context context, Intent intent) { if (action.equals("android.provider.Telephony.SMS_RECEIVED")) { - for (SmsMessage sms : getMessagesFromIntent(intent)) { - app.sendMessageToServer(sms); + for (IncomingMessage sms : getMessagesFromIntent(intent)) { + app.forwardToServer(sms); } if (!app.getKeepInInbox()) @@ -37,15 +39,16 @@ public void onReceive(Context context, Intent intent) { // from http://github.com/dimagi/rapidandroid // source: http://www.devx.com/wireless/Article/39495/1954 - private SmsMessage[] getMessagesFromIntent(Intent intent) { - SmsMessage retMsgs[] = null; - Bundle bdl = intent.getExtras(); - Object pdus[] = (Object[]) bdl.get("pdus"); - retMsgs = new SmsMessage[pdus.length]; - for (int n = 0; n < pdus.length; n++) { - byte[] byteData = (byte[]) pdus[n]; - retMsgs[n] = SmsMessage.createFromPdu(byteData); + private List getMessagesFromIntent(Intent intent) + { + Bundle bundle = intent.getExtras(); + List messages = new ArrayList(); + + for (Object pdu : (Object[]) bundle.get("pdus")) + { + SmsMessage sms = SmsMessage.createFromPdu((byte[]) pdu); + messages.add(new IncomingMessage(app, sms)); } - return retMsgs; + return messages; } } \ No newline at end of file diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java index 0fb931f..fac47fc 100755 --- a/src/org/envaya/kalsms/Main.java +++ b/src/org/envaya/kalsms/Main.java @@ -34,14 +34,13 @@ public void onReceive(Context context, Intent intent) { private class TestTask extends HttpTask { public TestTask() { - super(app); + super(Main.this.app, new BasicNameValuePair("action", App.ACTION_OUTGOING)); } @Override protected void handleResponse(HttpResponse response) throws Exception { - parseResponseXML(response); - + parseResponseXML(response); app.log("Server connection OK!"); } } @@ -138,9 +137,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; case R.id.test: app.log("Testing server connection..."); - new TestTask().execute( - new BasicNameValuePair("action", App.ACTION_OUTGOING) - ); + new TestTask().execute(); return true; default: return super.onOptionsItemSelected(item); diff --git a/src/org/envaya/kalsms/OutgoingMessage.java b/src/org/envaya/kalsms/OutgoingMessage.java new file mode 100755 index 0000000..335eb95 --- /dev/null +++ b/src/org/envaya/kalsms/OutgoingMessage.java @@ -0,0 +1,107 @@ + +package org.envaya.kalsms; + +import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; +import android.telephony.SmsManager; + +public class OutgoingMessage extends QueuedMessage { + + private String serverId; + private String message; + private String from; + private String to; + + private String localId; + + private static int nextLocalId = 1; + + public OutgoingMessage(App app) + { + super(app); + this.localId = "_o" + getNextLocalId(); + } + + static synchronized int getNextLocalId() + { + return nextLocalId++; + } + + public String getId() + { + return (serverId == null) ? localId : serverId; + } + + public String getLogName() + { + return (serverId == null) ? "SMS reply" : ("SMS id=" + serverId); + } + + public String getServerId() + { + return serverId; + } + + public void setServerId(String id) + { + this.serverId = id; + } + + public String getMessageBody() + { + return message; + } + + public void setMessageBody(String message) + { + this.message = message; + } + + public String getFrom() + { + return from; + } + + public void setFrom(String from) + { + this.from = from; + } + + public String getTo() + { + return to; + } + + public void setTo(String to) + { + this.to = to; + } + + public void retryNow() { + app.log("Retrying sending " + getLogName() + " to " + getTo()); + trySend(); + } + + public void trySend() + { + SmsManager smgr = SmsManager.getDefault(); + + Intent intent = new Intent(app.context, MessageStatusNotifier.class); + intent.setData(Uri.parse("kalsms://outgoing/" + getId())); + + PendingIntent sentIntent = PendingIntent.getBroadcast( + app.context, + 0, + intent, + PendingIntent.FLAG_ONE_SHOT); + + smgr.sendTextMessage(getTo(), null, getMessageBody(), sentIntent, null); + } + + protected Intent getRetryIntent() { + Intent intent = new Intent(app.context, OutgoingMessageRetry.class); + intent.setData(Uri.parse("kalsms://outgoing/" + getId())); + return intent; + } +} diff --git a/src/org/envaya/kalsms/OutgoingSmsMessage.java b/src/org/envaya/kalsms/OutgoingSmsMessage.java deleted file mode 100755 index 8b7ddf5..0000000 --- a/src/org/envaya/kalsms/OutgoingSmsMessage.java +++ /dev/null @@ -1,75 +0,0 @@ - -package org.envaya.kalsms; - -public class OutgoingSmsMessage { - - private String serverId; - private String message; - private String from; - 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() - { - return (serverId == null) ? "SMS reply" : ("SMS id=" + serverId); - } - - public String getServerId() - { - return serverId; - } - - public void setServerId(String id) - { - this.serverId = id; - } - - public String getMessage() - { - return message; - } - - public void setMessage(String message) - { - this.message = message; - } - - public String getFrom() - { - return from; - } - - public void setFrom(String from) - { - this.from = from; - } - - public String getTo() - { - return to; - } - - public void setTo(String to) - { - this.to = to; - } - -} diff --git a/src/org/envaya/kalsms/PollerTask.java b/src/org/envaya/kalsms/PollerTask.java new file mode 100755 index 0000000..b7a1526 --- /dev/null +++ b/src/org/envaya/kalsms/PollerTask.java @@ -0,0 +1,19 @@ + +package org.envaya.kalsms; + +import org.apache.http.HttpResponse; +import org.apache.http.message.BasicNameValuePair; + +public class PollerTask extends HttpTask { + + public PollerTask(App app) { + super(app, new BasicNameValuePair("action", App.ACTION_OUTGOING)); + } + + @Override + protected void handleResponse(HttpResponse response) throws Exception { + for (OutgoingMessage reply : parseResponseXML(response)) { + app.sendOutgoingMessage(reply); + } + } +} \ No newline at end of file diff --git a/src/org/envaya/kalsms/QueuedMessage.java b/src/org/envaya/kalsms/QueuedMessage.java new file mode 100755 index 0000000..7d887e8 --- /dev/null +++ b/src/org/envaya/kalsms/QueuedMessage.java @@ -0,0 +1,69 @@ +package org.envaya.kalsms; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.SystemClock; + +public abstract class QueuedMessage +{ + protected long nextRetryTime = 0; + protected int numRetries = 0; + + public App app; + + public QueuedMessage(App app) + { + this.app = app; + } + + public boolean canRetryNow() { + return (nextRetryTime > 0 && nextRetryTime < SystemClock.elapsedRealtime()); + } + + public boolean scheduleRetry() { + long now = SystemClock.elapsedRealtime(); + numRetries++; + + if (numRetries > 4) { + app.log("5th failure: giving up"); + return false; + } + + int second = 1000; + int minute = second * 60; + + if (numRetries == 1) { + app.log("1st failure; retry in 1 minute"); + nextRetryTime = now + 1 * minute; + } else if (numRetries == 2) { + app.log("2nd failure; retry in 10 minutes"); + nextRetryTime = now + 10 * minute; + } else if (numRetries == 3) { + app.log("3rd failure; retry in 1 hour"); + nextRetryTime = now + 60 * minute; + } else { + app.log("4th failure: retry in 1 day"); + nextRetryTime = now + 24 * 60 * minute; + } + + AlarmManager alarm = (AlarmManager) app.context.getSystemService(Context.ALARM_SERVICE); + + PendingIntent pendingIntent = PendingIntent.getBroadcast(app.context, + 0, + getRetryIntent(), + 0); + + alarm.set( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + nextRetryTime, + pendingIntent); + + return true; + } + + public abstract void retryNow(); + + protected abstract Intent getRetryIntent(); +} From 61d24b2f64c3d1c4e27a737eab798e8c422e36f2 Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Tue, 13 Sep 2011 16:06:50 -0700 Subject: [PATCH 23/69] add setting to enable/disable sms gateway --- res/xml/prefs.xml | 9 +++++++ src/org/envaya/kalsms/App.java | 26 ++++++++++++------- src/org/envaya/kalsms/BootReceiver.java | 6 ++++- .../kalsms/IncomingMessageForwarder.java | 5 ++++ src/org/envaya/kalsms/Main.java | 15 ++++++----- src/org/envaya/kalsms/Prefs.java | 5 ++++ 6 files changed, 49 insertions(+), 17 deletions(-) diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index 478996c..3005176 100755 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -1,6 +1,15 @@ + + + 0) { - alarm.setRepeating( - AlarmManager.ELAPSED_REALTIME_WAKEUP, - SystemClock.elapsedRealtime(), - pollSeconds * 1000, - pendingIntent); - log("Checking for outgoing messages every " + pollSeconds + " sec"); - } else { - log("Not checking for outgoing messages."); + if (isEnabled()) + { + if (pollSeconds > 0) { + alarm.setRepeating( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime(), + pollSeconds * 1000, + pendingIntent); + log("Checking for outgoing messages every " + pollSeconds + " sec"); + } else { + log("Not checking for outgoing messages."); + } } } @@ -103,6 +106,11 @@ public boolean getLaunchOnBoot() { return settings.getBoolean("launch_on_boot", false); } + public boolean isEnabled() + { + return settings.getBoolean("enabled", false); + } + public boolean getKeepInInbox() { return settings.getBoolean("keep_in_inbox", false); diff --git a/src/org/envaya/kalsms/BootReceiver.java b/src/org/envaya/kalsms/BootReceiver.java index 407ed66..bacf187 100755 --- a/src/org/envaya/kalsms/BootReceiver.java +++ b/src/org/envaya/kalsms/BootReceiver.java @@ -11,7 +11,11 @@ public class BootReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { App app = App.getInstance(context.getApplicationContext()); - + if (!app.isEnabled()) + { + return; + } + app.setOutgoingMessageAlarm(); if (app.getLaunchOnBoot()) diff --git a/src/org/envaya/kalsms/IncomingMessageForwarder.java b/src/org/envaya/kalsms/IncomingMessageForwarder.java index 3c4d43d..85a44b0 100755 --- a/src/org/envaya/kalsms/IncomingMessageForwarder.java +++ b/src/org/envaya/kalsms/IncomingMessageForwarder.java @@ -18,6 +18,11 @@ public class IncomingMessageForwarder extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { app = App.getInstance(context.getApplicationContext()); + if (!app.isEnabled()) + { + return; + } + try { String action = intent.getAction(); diff --git a/src/org/envaya/kalsms/Main.java b/src/org/envaya/kalsms/Main.java index fac47fc..644135a 100755 --- a/src/org/envaya/kalsms/Main.java +++ b/src/org/envaya/kalsms/Main.java @@ -104,18 +104,19 @@ public void onCreate(Bundle savedInstanceState) { TextView info = (TextView) this.findViewById(R.id.info); - info.setText(Html.fromHtml("SMS Gateway running.
")); - info.append(Html.fromHtml("Press Menu to edit settings.
")); - - showLogMessage("Server URL is: " + app.getDisplayString(app.getServerUrl())); - showLogMessage("Your phone number is: " + app.getDisplayString(app.getPhoneNumber())); - info.setMovementMethod(new ScrollingMovementMethod()); IntentFilter logReceiverFilter = new IntentFilter(); logReceiverFilter.addAction(App.LOG_INTENT); - registerReceiver(logReceiver, logReceiverFilter); + registerReceiver(logReceiver, logReceiverFilter); + + info.append(Html.fromHtml( + app.isEnabled() ? "SMS gateway running.
" : "SMS gateway disabled.
")); + info.append("Server URL is: " + app.getDisplayString(app.getServerUrl()) + "\n"); + info.append("Your phone number is: " + app.getDisplayString(app.getPhoneNumber()) + "\n"); + info.append(Html.fromHtml("Press Menu to edit settings.
")); + app.setOutgoingMessageAlarm(); } diff --git a/src/org/envaya/kalsms/Prefs.java b/src/org/envaya/kalsms/Prefs.java index 4c2c905..de1c98a 100755 --- a/src/org/envaya/kalsms/Prefs.java +++ b/src/org/envaya/kalsms/Prefs.java @@ -60,6 +60,11 @@ else if (key.equals("password")) { app.log("Password changed"); } + else if (key.equals("enabled")) + { + app.log(app.isEnabled() ? "SMS Gateway started." : "SMS Gateway stopped."); + app.setOutgoingMessageAlarm(); + } updatePrefSummary(findPreference(key)); } From 41141fa5fd27ca8883a3ba5fbbca5ab49b6dab9e Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Tue, 13 Sep 2011 18:26:17 -0700 Subject: [PATCH 24/69] add menu option to forward existing messages from inbox (delivered when kalsms was not enabled) --- AndroidManifest.xml | 4 ++ res/drawable-hdpi/ic_menu_dialog.png | Bin 0 -> 1788 bytes res/drawable-ldpi/ic_menu_dialog.png | Bin 0 -> 944 bytes res/drawable-mdpi/ic_menu_dialog.png | Bin 0 -> 1134 bytes res/layout/inbox.xml | 18 ++++++ res/layout/inbox_item.xml | 31 +++++++++ res/menu/mainmenu.xml | 10 ++- res/values/strings.xml | 1 + src/org/envaya/kalsms/ForwardInbox.java | 69 +++++++++++++++++++++ src/org/envaya/kalsms/Help.java | 2 +- src/org/envaya/kalsms/IncomingMessage.java | 22 +++++-- src/org/envaya/kalsms/Main.java | 5 +- src/org/envaya/kalsms/Prefs.java | 9 +-- 13 files changed, 152 insertions(+), 19 deletions(-) create mode 100755 res/drawable-hdpi/ic_menu_dialog.png create mode 100755 res/drawable-ldpi/ic_menu_dialog.png create mode 100755 res/drawable-mdpi/ic_menu_dialog.png create mode 100755 res/layout/inbox.xml create mode 100755 res/layout/inbox_item.xml create mode 100755 src/org/envaya/kalsms/ForwardInbox.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2e41cf2..eca7b0a 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -23,6 +23,10 @@ + + + + diff --git a/res/drawable-hdpi/ic_menu_dialog.png b/res/drawable-hdpi/ic_menu_dialog.png new file mode 100755 index 0000000000000000000000000000000000000000..ee6e6854c34dc8e8b94b12170ed7da667394ce51 GIT binary patch literal 1788 zcmVKx@N{bvO}baGDz46V>toi33vmR zF&hSAyf!;TygU^o`4xFaKTp9X;)l23N0M zJ?1#hU&Amgnx+XM1c-Xu9cD%th6sWHrfIgv#>QT_a^=c>op+c$?NRs8!Gi~Xq?9U- zjEumxZ4gmk1Y%}*o`;Q%4b^V9e+uA5t&aAzN8KY978c%h9A_+_&-X=~ghZrPtA%E> z*<4#&E6&f)N8LU@EC+G1SR6~719S$`FNi2{Y2oP6qay%5?DpAV2^2{wK}3BOW+!pL zhGkh=4Gv2n7={6$zs`A*G}GAVLqB$fnTeTAB7$KU&;0BhR1#12^3i1&4iW$yrPQ{I zo<=gLD2le1>nQ3vzV?KH3dGEWjzGB!7cP7bK<*ij(E9rN`1<<#XCFR%_?4}#t!dYF z2P)D)1tKD5<^aG30I!SosZ=VxQ!15~8;!>6ckkZ)VGso8q9{royXy72*6#;C#fT_m z<}CoNZXW?49LK32KYn~}X=&+CMD)o)xhPe3cJt=Vsd~Ns<5sKnLJ$O>=yj(6fQUX& zO5Kg3Xzu#;>#OK2D7o9WZ@;>^x%s`c0(qWy=+dQ2UE4_8K?@5D(`##M%f9dDP1D5A zWUp7mHPm>!-Tsmg;>EeSxl=c8+<3suE`UGoWs2#dEzqM!j}~&dTz+h93{uKxu0&~x zq6n_*qS0s^5<=Vn@G^i<2=QjxB|8mT0?p6Q8&b;87YYR^rTSh&*|rVKve0NWP606D z_|;fkTul95DWz72wf_F`@o}e6C=`-10q9Ng_Oyg9si7RlDFNX4JKoDXbGe)mryE_N z)@jF>MMXx-&aFTLaJr-~tGB z+ym@0fruz#=47xmJw5&JLZNWs-o1Ob0DKX^wn*_d$y)##0DN6x682q?CL$)Hh=@V} z-o(VjyR};FYq?zRXSQv_w(afgUI_6P08T58wm|iIedq%1Bp$dApnmb<#qSi0#c$cR z?Il56DRm!lf;kX@fZ;pnNn()#0FTa{JNJuHsq~6%+yAj`J3M*vJC0+xt~>BR9Vdi>nIj^4sw3iJwppoE zmMfLYR{^y55`O><2Qf1pK72S7LU2-x4Qgt|gOuI$Hx|$Zut`LIN?o?mnqt*z)t6Gf zlUy_yggAy_KuY-l9f4@Ksg@8=xt`cuHxHshq3~-Wdetxt1VMlx2zFa#Qg5gwrGk_a z2M!$gL;Q8rtATgj51k(~v$eXq`sq7&?)-+Ck9nSlcDoJF^FAVy5CXPs!!*rQBJC(N z|1~o+^HRB7{wI16Q*_aFP%t?;xfTS$OUuj4-)H78_`d%sB6_S2GEEaoDdh8cn5L<{ z>&VO?qKAfI{Q1O*6W=YD%c}s~L5l;VIMv|)9sVf*>ej7Wueq)}6Nceq6|_={i^bv( zYPH(W0ff5Epp(D2=o}CEyRBaRM7tJC5;JCQ0`Qpmei()`%)C9Vq?Cc@=WMpJyWMpJyWMpJy eWMpJ?#eV@a8Kh)VIqs+c0000Lh literal 0 HcmV?d00001 diff --git a/res/drawable-ldpi/ic_menu_dialog.png b/res/drawable-ldpi/ic_menu_dialog.png new file mode 100755 index 0000000000000000000000000000000000000000..efa71eaaeef75068d785f4dd41c224aa2c12f160 GIT binary patch literal 944 zcmV;h15f;kP)|VXTF9Jgy<$h!@bK`pjg5^lRaMW2sKo#>U0q%G z2L}f~)h>qpvV)>xvG^<+jb5-UD^%X*=H}&GE;k0C?<9fPw(Xu|G6|w^jb>(Kv)Nw1 zwc`MxqoacqMe*H`nd{~{j^i6YG7vNC3kwT{S|BNp-@1z8t7f+a)p^2ax4*XOH0e=wzjrMqobqmr>3UPRIAk=p67{xl=8s9Kx=K#w?p}SeoRW) z8;ivno*-tX?d|PLTU%St0bJ?t@86i1m{hJMw^idQAwrvvt zFmp>XnT#b82~Sm3c%BDlMm!$(9LM=$7{(I-e;LfT1+A^E2y4cDMIsUGb*IYA-I+|L zS4#P2b#?WOloC5TJFois`tAX!5>aD7M}?^s5tW(w?n^0Uw%+@Ju01N0-&I0Hl9}Jk&dxqqUtj+m)E!?SW>zLACria* zF%lL^Q4|OvO3bX(`eV;iFR?d)fi-+$WEjRwdwVik!kKr@UjVkaM zW@c@Ce0;oAD%}-AAdyHcnWlLol}i1rQ_lt99@d3=s&^E-nx=gfLcnpHB_YK1R4TQ6 zoXw641k19%8-|gMMx!^<>GY|?00CDAuzP!3mNh4YNT<{3rGEj_R8vj;m-+>l%5&N@ S9j&SW0000G5anM0@O`+ElXyEh8YP7p)P`=d=eC#)IME+5t_!q zN(@6KKhj-9zCP!?etqxp4G<6z5D*aXKZg7ame1!!yWL*6xVTuVR4Nm;ZTB2Yk|ZP& ziT6W8Lu*2a{KCRQyVJZ-&lx&k+x9nSXJ@OHWkCqhcf3ZUG0|)`S4T%j0bs4u9A5>L zN~NV_GKowk15MLF)VsmVjH|0F6pKZaN~KqL3IXA8_{GS`2oy!>t9S=RQJ`rW&d<*$ z-RAfzAe~O%y*kY7OH3gI1_uXSuS>pU0l>@@i^V)WdE2%<4`zm@X|A#SbU+Lb4?~jN zpNVbTFijKndL8X{+ZUJo6wscUnu>I}PKSqwp9A>HFpTfZ<#MLgYW1DeHvvS%0L*T4 znE3;M)1#xK->$E(|7x{bpY`j4tAKnyFB*-;e6d(us@Ln2{UjnH3=9mMtgNi8#bU9a z=jZ4DE))tsFmwLV1XQclh4b_CRm-yaN&wvKNY!d}^78U>ZES3e0c?aqq2K+8?JA&L zF2719lSrjfzBQ#%sf0?UvP4Ad%zU=Jz1?$62iGk_QIyGCE{AwL4j}|Wp-|652!VJ! zj$AGWMNwV=H*3c0HY$VwGk2`j)qzYV^C(RznM~gOekd*xfry~0>RY&cc)B{^G!JI> zZ2&+t8g=55BuS7Y2}JatxkQ9WBm%=Q5RFD(yVdFH!2P6;gve&IkR%DUS`D>Y4bGw_ zBFC-wMD%Mmn_VZOjw`;afJdf1Jv|)(5IQ(G_|dlQ#d^IC%d)`Cux64a~Sc$y~bxU^YZ@wKA3sYae|260chXObLCSya0d~Y%)Gp}w|6H3z=@Z8KbZp` ze*LPQ9M!(e8Kh&oz) zE8<%K_CxPK;O+oLB9XVIX@23?cIq^xNF?Igl zLzZQzs)|%91zp!&n^G@ybbyGg + + +