From d994b10c351718eff133a8a37ce107327eb12833 Mon Sep 17 00:00:00 2001 From: Jesse Young Date: Sat, 17 Sep 2011 18:38:16 -0700 Subject: [PATCH] support for incoming MMS --- AndroidManifest.xml | 16 +- libs/httpmime-4.1.2.jar | Bin 0 -> 26890 bytes res/xml/prefs.xml | 3 +- src/org/envaya/kalsms/App.java | 136 +++++++++----- .../envaya/kalsms/CheckMmsInboxService.java | 49 +++++ src/org/envaya/kalsms/IncomingMessage.java | 59 ++---- src/org/envaya/kalsms/IncomingMms.java | 163 ++++++++++++++++ src/org/envaya/kalsms/IncomingSms.java | 56 ++++++ src/org/envaya/kalsms/MmsObserver.java | 51 +++++ src/org/envaya/kalsms/MmsPart.java | 113 +++++++++++ src/org/envaya/kalsms/MmsUtils.java | 176 ++++++++++++++++++ src/org/envaya/kalsms/OutgoingMessage.java | 8 +- src/org/envaya/kalsms/QueuedMessage.java | 3 + .../envaya/kalsms/receiver/BootReceiver.java | 9 +- .../kalsms/receiver/IncomingMessageRetry.java | 2 +- .../envaya/kalsms/receiver/MMSReceiver.java | 27 --- .../receiver/MessageStatusNotifier.java | 5 +- .../kalsms/receiver/OutgoingMessageRetry.java | 2 +- .../envaya/kalsms/receiver/SMSReceiver.java | 5 +- src/org/envaya/kalsms/task/ForwarderTask.java | 2 +- src/org/envaya/kalsms/task/HttpTask.java | 43 ++++- src/org/envaya/kalsms/ui/ForwardInbox.java | 3 +- src/org/envaya/kalsms/ui/Main.java | 15 +- 23 files changed, 787 insertions(+), 159 deletions(-) create mode 100755 libs/httpmime-4.1.2.jar create mode 100755 src/org/envaya/kalsms/CheckMmsInboxService.java create mode 100755 src/org/envaya/kalsms/IncomingMms.java create mode 100755 src/org/envaya/kalsms/IncomingSms.java create mode 100755 src/org/envaya/kalsms/MmsObserver.java create mode 100755 src/org/envaya/kalsms/MmsPart.java create mode 100755 src/org/envaya/kalsms/MmsUtils.java delete mode 100755 src/org/envaya/kalsms/receiver/MMSReceiver.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 957994e..246e776 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -33,19 +33,12 @@ android:label="@string/app_name"> - + - - - - - - - - - + + @@ -63,5 +56,8 @@ + + + \ No newline at end of file diff --git a/libs/httpmime-4.1.2.jar b/libs/httpmime-4.1.2.jar new file mode 100755 index 0000000000000000000000000000000000000000..eea3b3ff1761f4683a527726887a193ffbf7af49 GIT binary patch literal 26890 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 literal 0 HcmV?d00001 diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml index 68d0d66..5539904 100755 --- a/res/xml/prefs.xml +++ b/res/xml/prefs.xml @@ -41,5 +41,6 @@ android:title="Keep new messages?" android:summaryOff="Incoming SMS will not be stored in Messaging inbox" android:summaryOn="Incoming SMS will be stored in Messaging inbox" - > + > + \ No newline at end of file diff --git a/src/org/envaya/kalsms/App.java b/src/org/envaya/kalsms/App.java index aebcad5..651019e 100755 --- a/src/org/envaya/kalsms/App.java +++ b/src/org/envaya/kalsms/App.java @@ -10,9 +10,11 @@ import android.app.PendingIntent; 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.text.Html; import android.text.SpannableStringBuilder; import android.util.Log; import java.text.DateFormat; @@ -26,24 +28,56 @@ public final class App extends Application { 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 MESSAGE_TYPE_MMS = "mms"; + public static final String MESSAGE_TYPE_SMS = "sms"; + public static final String LOG_NAME = "KALSMS"; public static final String LOG_INTENT = "org.envaya.kalsms.LOG"; public static final int MAX_DISPLAYED_LOG = 15000; public static final int LOG_TIMESTAMP_INTERVAL = 60000; - private long lastLogTime = 0; - private SpannableStringBuilder displayedLog = new SpannableStringBuilder(); - private Map incomingSmsMap = new HashMap(); - private Map outgoingSmsMap = new HashMap(); + // Each QueuedMessage is identified within our internal Map by its Uri. + // Currently QueuedMessage instances are only available within KalSMS, + // (but they could be made available to other applications later via a ContentProvider) + public static final Uri CONTENT_URI = Uri.parse("content://org.envaya.kalsms"); + public static final Uri INCOMING_URI = Uri.withAppendedPath(CONTENT_URI, "incoming"); + public static final Uri OUTGOING_URI = Uri.withAppendedPath(CONTENT_URI, "outgoing"); + + private Map incomingMessages = new HashMap(); + private Map outgoingMessages = new HashMap(); - public SharedPreferences getSettings() + private SharedPreferences settings; + private MmsObserver mmsObserver; + private SpannableStringBuilder displayedLog = new SpannableStringBuilder(); + private long lastLogTime; + + private MmsUtils mmsUtils; + + @Override + public void onCreate() { - return PreferenceManager.getDefaultSharedPreferences(this); - } + super.onCreate(); + + settings = PreferenceManager.getDefaultSharedPreferences(this); + mmsUtils = new MmsUtils(this); + + log(Html.fromHtml( + isEnabled() ? "SMS gateway running." : "SMS gateway disabled.")); + + log("Server URL is: " + getDisplayString(getServerUrl())); + log("Your phone number is: " + getDisplayString(getPhoneNumber())); + + mmsObserver = new MmsObserver(this); + mmsObserver.register(); + + setOutgoingMessageAlarm(); + } public void checkOutgoingMessages() { @@ -92,29 +126,29 @@ public final class App extends Application { } public String getServerUrl() { - return getSettings().getString("server_url", ""); + return settings.getString("server_url", ""); } public String getPhoneNumber() { - return getSettings().getString("phone_number", ""); + return settings.getString("phone_number", ""); } public int getOutgoingPollSeconds() { - return Integer.parseInt(getSettings().getString("outgoing_interval", "0")); + return Integer.parseInt(settings.getString("outgoing_interval", "0")); } public boolean isEnabled() { - return getSettings().getBoolean("enabled", false); + return settings.getBoolean("enabled", false); } public boolean getKeepInInbox() { - return getSettings().getBoolean("keep_in_inbox", false); + return settings.getBoolean("keep_in_inbox", false); } public String getPassword() { - return getSettings().getString("password", ""); + return settings.getString("password", ""); } private void notifyStatus(OutgoingMessage sms, String status, String errorMessage) { @@ -150,35 +184,45 @@ public final class App extends Application { } public synchronized int getStuckMessageCount() { - return outgoingSmsMap.size() + incomingSmsMap.size(); + return outgoingMessages.size() + incomingMessages.size(); } public synchronized void retryStuckOutgoingMessages() { - for (OutgoingMessage sms : outgoingSmsMap.values()) { + for (OutgoingMessage sms : outgoingMessages.values()) { sms.retryNow(); } } public synchronized void retryStuckIncomingMessages() { - for (IncomingMessage sms : incomingSmsMap.values()) { + for (IncomingMessage sms : incomingMessages.values()) { sms.retryNow(); } } - public synchronized void setIncomingMessageStatus(IncomingMessage sms, boolean success) { - String id = sms.getId(); + public synchronized void setIncomingMessageStatus(IncomingMessage message, boolean success) { + Uri uri = message.getUri(); if (success) { - incomingSmsMap.remove(id); + incomingMessages.remove(uri); + + if (message instanceof IncomingMms) + { + IncomingMms mms = (IncomingMms)message; + if (!getKeepInInbox()) + { + log("Deleting MMS " + mms.getId() + " from inbox..."); + mmsUtils.deleteFromInbox(mms); + } + } } - else if (!sms.scheduleRetry()) + else if (!message.scheduleRetry()) { - incomingSmsMap.remove(id); + incomingMessages.remove(uri); } } - public synchronized void notifyOutgoingMessageStatus(String id, int resultCode) { - OutgoingMessage sms = outgoingSmsMap.get(id); + public synchronized void notifyOutgoingMessageStatus(Uri uri, int resultCode) { + OutgoingMessage sms = outgoingMessages.get(uri); if (sms == null) { return; @@ -210,52 +254,52 @@ public final class App extends Application { case SmsManager.RESULT_ERROR_RADIO_OFF: case SmsManager.RESULT_ERROR_NO_SERVICE: if (!sms.scheduleRetry()) { - outgoingSmsMap.remove(id); + outgoingMessages.remove(uri); } break; default: - outgoingSmsMap.remove(id); + outgoingMessages.remove(uri); break; } } public synchronized void sendOutgoingMessage(OutgoingMessage sms) { - String id = sms.getId(); - if (outgoingSmsMap.containsKey(id)) { + Uri uri = sms.getUri(); + if (outgoingMessages.containsKey(uri)) { log(sms.getLogName() + " already sent, skipping"); return; } - outgoingSmsMap.put(id, sms); + outgoingMessages.put(uri, sms); log("Sending " + sms.getLogName() + " to " + sms.getTo()); sms.trySend(); } - public synchronized void forwardToServer(IncomingMessage sms) { - String id = sms.getId(); + public synchronized void forwardToServer(IncomingMessage message) { + Uri uri = message.getUri(); - if (incomingSmsMap.containsKey(id)) { - log("Duplicate incoming SMS, skipping"); + if (incomingMessages.containsKey(uri)) { + log("Duplicate incoming "+message.getDisplayType()+", skipping"); return; } - incomingSmsMap.put(id, sms); + incomingMessages.put(uri, message); - log("Received SMS from " + sms.getFrom()); + log("Received "+message.getDisplayType()+" from " + message.getFrom()); - sms.tryForwardToServer(); + message.tryForwardToServer(); } - public synchronized void retryIncomingMessage(String id) { - IncomingMessage sms = incomingSmsMap.get(id); - if (sms != null) { - sms.retryNow(); + public synchronized void retryIncomingMessage(Uri uri) { + IncomingMessage message = incomingMessages.get(uri); + if (message != null) { + message.retryNow(); } } - public synchronized void retryOutgoingMessage(String id) { - OutgoingMessage sms = outgoingSmsMap.get(id); + public synchronized void retryOutgoingMessage(Uri uri) { + OutgoingMessage sms = outgoingMessages.get(uri); if (sms != null) { sms.retryNow(); } @@ -265,7 +309,7 @@ public final class App extends Application { Log.d(LOG_NAME, msg); } - public void log(CharSequence msg) + public synchronized void log(CharSequence msg) { Log.d(LOG_NAME, msg.toString()); @@ -303,7 +347,7 @@ public final class App extends Application { sendBroadcast(broadcast); } - public CharSequence getDisplayedLog() + public synchronized CharSequence getDisplayedLog() { return displayedLog; } @@ -328,6 +372,10 @@ public final class App extends Application { logError("Inner exception:", innerEx, true); } } + } + + public MmsUtils getMmsUtils() + { + return mmsUtils; } - } diff --git a/src/org/envaya/kalsms/CheckMmsInboxService.java b/src/org/envaya/kalsms/CheckMmsInboxService.java new file mode 100755 index 0000000..e808bff --- /dev/null +++ b/src/org/envaya/kalsms/CheckMmsInboxService.java @@ -0,0 +1,49 @@ + +package org.envaya.kalsms; + +import android.app.IntentService; +import android.content.Intent; +import java.util.List; + +public class CheckMmsInboxService extends IntentService +{ + private App app; + private MmsUtils mmsUtils; + + public CheckMmsInboxService(String name) + { + super(name); + } + + public CheckMmsInboxService() + { + this("CheckMmsInboxService"); + } + + @Override + public void onCreate() { + super.onCreate(); + + app = (App)this.getApplicationContext(); + + mmsUtils = app.getMmsUtils(); + } + + @Override + protected void onHandleIntent(Intent intent) + { + List messages = mmsUtils.getMessagesInInbox(); + for (IncomingMms mms : messages) + { + if (mmsUtils.isNewMms(mms)) + { + // prevent forwarding MMS messages that existed in inbox + // before KalSMS started, or re-forwarding MMS multiple + // times if we don't delete them. + mmsUtils.markOldMms(mms); + + app.forwardToServer(mms); + } + } + } +} \ No newline at end of file diff --git a/src/org/envaya/kalsms/IncomingMessage.java b/src/org/envaya/kalsms/IncomingMessage.java index 5c47c6a..85815c5 100755 --- a/src/org/envaya/kalsms/IncomingMessage.java +++ b/src/org/envaya/kalsms/IncomingMessage.java @@ -1,32 +1,21 @@ package org.envaya.kalsms; -import org.envaya.kalsms.task.ForwarderTask; -import org.envaya.kalsms.receiver.IncomingMessageRetry; import android.content.Intent; import android.net.Uri; -import android.telephony.SmsMessage; -import org.apache.http.message.BasicNameValuePair; +import org.envaya.kalsms.receiver.IncomingMessageRetry; -public class IncomingMessage extends QueuedMessage { +public abstract class IncomingMessage extends QueuedMessage { - public String from; - public String message; - public long timestampMillis; - - public IncomingMessage(App app, SmsMessage sms) { - super(app); - this.from = sms.getOriginatingAddress(); - this.message = sms.getMessageBody(); - this.timestampMillis = sms.getTimestampMillis(); - } + protected String from; - public IncomingMessage(App app, String from, String message, long timestampMillis) { + public IncomingMessage(App app, String from) + { super(app); this.from = from; - this.message = message; - this.timestampMillis = timestampMillis; - } - + } + + public abstract String getDisplayType(); + public boolean isForwardable() { /* @@ -36,37 +25,21 @@ public class IncomingMessage extends QueuedMessage { return from.length() > 5; } - public String getMessageBody() - { - return message; - } - public String getFrom() { return from; } - public String getId() - { - return from + ":" + message + ":" + timestampMillis; - } - public void retryNow() { - app.log("Retrying forwarding SMS from " + from); + app.log("Retrying forwarding message from " + from); tryForwardToServer(); - } - - public void tryForwardToServer() { - new ForwarderTask(this, - new BasicNameValuePair("from", getFrom()), - new BasicNameValuePair("message", getMessageBody()) - ).execute(); - } - - + } + protected Intent getRetryIntent() { Intent intent = new Intent(app, IncomingMessageRetry.class); - intent.setData(Uri.parse("kalsms://incoming/" + this.getId())); + intent.setData(this.getUri()); return intent; - } + } + + public abstract void tryForwardToServer(); } diff --git a/src/org/envaya/kalsms/IncomingMms.java b/src/org/envaya/kalsms/IncomingMms.java new file mode 100755 index 0000000..36b090e --- /dev/null +++ b/src/org/envaya/kalsms/IncomingMms.java @@ -0,0 +1,163 @@ + +package org.envaya.kalsms; + +import android.net.Uri; +import java.io.IOException; +import java.util.ArrayList; + +import org.json.*; + +import java.util.List; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.content.ByteArrayBody; +import org.apache.http.entity.mime.content.ContentBody; +import org.apache.http.entity.mime.content.InputStreamBody; +import org.apache.http.message.BasicNameValuePair; +import org.envaya.kalsms.task.ForwarderTask; + +public class IncomingMms extends IncomingMessage { + List parts; + long id; + String contentLocation; + + public IncomingMms(App app, String from, long id) + { + super(app, from); + this.parts = new ArrayList(); + this.id = id; + } + + public String getDisplayType() + { + return "MMS"; + } + + public List getParts() + { + return parts; + } + + public void addPart(MmsPart part) + { + parts.add(part); + } + + public long getId() + { + return id; + } + + public String getContentLocation() + { + return contentLocation; + } + + public void setContentLocation(String contentLocation) + { + this.contentLocation = contentLocation; + } + + + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append("MMS id="); + builder.append(id); + builder.append(" from="); + builder.append(from); + builder.append(":\n"); + + for (MmsPart part : parts) + { + builder.append(" "); + builder.append(part.toString()); + builder.append("\n"); + } + return builder.toString(); + } + + public void tryForwardToServer() + { + app.log("Forwarding MMS to server..."); + + List formParts = new ArrayList(); + + int i = 0; + + String message = ""; + JSONArray partsMetadata = new JSONArray(); + + for (MmsPart part : parts) + { + String formFieldName = "part" + i; + String text = part.getText(); + String contentType = part.getContentType(); + String partName = part.getName(); + + if ("text/plain".equals(contentType)) + { + message = text; + } + + ContentBody body; + + if (text != null) + { + if (contentType != null) + { + contentType += "; charset=utf-8"; + } + + body = new ByteArrayBody(text.getBytes(), contentType, partName); + } + else + { + try + { + body = new InputStreamBody(part.openInputStream(), + contentType, partName); + } + catch (IOException ex) + { + app.logError("Error opening data for " + part.toString(), ex); + continue; + } + } + + try + { + JSONObject partMetadata = new JSONObject(); + partMetadata.put("name", formFieldName); + partMetadata.put("cid", part.getContentId()); + partMetadata.put("type", part.getContentType()); + partMetadata.put("filename", part.getName()); + partsMetadata.put(partMetadata); + } + catch (JSONException ex) + { + app.logError("Error encoding MMS part metadata for " + part.toString(), ex); + continue; + } + + + formParts.add(new FormBodyPart(formFieldName, body)); + i++; + } + + ForwarderTask task = new ForwarderTask(this, + new BasicNameValuePair("from", getFrom()), + new BasicNameValuePair("message", message), + new BasicNameValuePair("message_type", App.MESSAGE_TYPE_MMS), + new BasicNameValuePair("mms_parts", partsMetadata.toString()) + ); + + task.setFormParts(formParts); + task.execute(); + } + + public Uri getUri() + { + return Uri.withAppendedPath(App.INCOMING_URI, "mms/" + id); + } +} diff --git a/src/org/envaya/kalsms/IncomingSms.java b/src/org/envaya/kalsms/IncomingSms.java new file mode 100755 index 0000000..e2e9144 --- /dev/null +++ b/src/org/envaya/kalsms/IncomingSms.java @@ -0,0 +1,56 @@ + +package org.envaya.kalsms; + +import android.net.Uri; +import android.telephony.SmsMessage; +import org.apache.http.message.BasicNameValuePair; +import org.envaya.kalsms.task.ForwarderTask; + + +public class IncomingSms extends IncomingMessage { + + protected String message; + protected long timestampMillis; + + // constructor for SMS retrieved from android.provider.Telephony.SMS_RECEIVED intent + public IncomingSms(App app, SmsMessage sms) { + super(app, sms.getOriginatingAddress()); + this.message = sms.getMessageBody(); + this.timestampMillis = sms.getTimestampMillis(); + } + + // constructor for SMS retrieved from Messaging inbox + public IncomingSms(App app, String from, String message, long timestampMillis) { + super(app, from); + this.message = message; + this.timestampMillis = timestampMillis; + } + + public String getMessageBody() + { + return message; + } + + public String getDisplayType() + { + return "SMS"; + } + + public Uri getUri() + { + return Uri.withAppendedPath(App.INCOMING_URI, + "sms/" + + Uri.encode(from) + "/" + + timestampMillis + "/" + + Uri.encode(message)); + } + + public void tryForwardToServer() { + new ForwarderTask(this, + new BasicNameValuePair("from", getFrom()), + new BasicNameValuePair("message_type", App.MESSAGE_TYPE_SMS), + new BasicNameValuePair("message", getMessageBody()) + ).execute(); + } + +} diff --git a/src/org/envaya/kalsms/MmsObserver.java b/src/org/envaya/kalsms/MmsObserver.java new file mode 100755 index 0000000..713647d --- /dev/null +++ b/src/org/envaya/kalsms/MmsObserver.java @@ -0,0 +1,51 @@ +package org.envaya.kalsms; + +import android.app.IntentService; +import android.content.Intent; +import android.database.ContentObserver; +import android.os.Handler; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +final class MmsObserver extends ContentObserver { + + private App app; + + public MmsObserver(App app) { + super(new Handler()); + this.app = app; + } + + public void register() + { + /* + * Content observers can watch the MMS inbox URI for changes; + * This is the URL passed to PduPersister.persist by + * com.android.mms.transaction.RetrieveTransaction.run + */ + app.getContentResolver().registerContentObserver( + MmsUtils.OBSERVER_URI, true, this); + app.log("MMS content observer registered"); + + MmsUtils mmsUtils = app.getMmsUtils(); + + List messages = mmsUtils.getMessagesInInbox(); + for (IncomingMms mms : messages) + { + mmsUtils.markOldMms(mms); + } + } + + @Override + public void onChange(final boolean selfChange) { + super.onChange(selfChange); + + if (!selfChange) + { + // check MMS inbox in an IntentService since it may be slow + // and we only want to do one check at a time + app.startService(new Intent(app, CheckMmsInboxService.class)); + } + } +} diff --git a/src/org/envaya/kalsms/MmsPart.java b/src/org/envaya/kalsms/MmsPart.java new file mode 100755 index 0000000..fe38bae --- /dev/null +++ b/src/org/envaya/kalsms/MmsPart.java @@ -0,0 +1,113 @@ +package org.envaya.kalsms; + +import android.net.Uri; +import java.io.FileNotFoundException; +import java.io.InputStream; + +public class MmsPart { + private App app; + private long partId; + private String contentType; + private String name; + private String text; + private String cid; + + public MmsPart(App app, long partId) + { + this.app = app; + this.partId = partId; + } + + /* + * The part id is the local value of the _id column in the MMS part table + * (see android.provider.Telephony.Part) + */ + public long getPartId() + { + return partId; + } + + /* + * The content id of a MMS part is used to resolve references in SMIL + * like . Telephony.java claims + * that the cid column is an integer, but it is actually a string + * like "<0000>" or "<83>". + */ + public void setContentId(String cid) + { + this.cid = cid; + } + + public String getContentId() + { + return cid; + } + + /* + * Common Content-Type values for MMS parts include: + * application/smil + * text/plain + * image/jpeg + */ + public void setContentType(String contentType) + { + this.contentType = contentType; + } + + public String getContentType() + { + return contentType; + } + + /* + * The name of an MMS part is the filename of the original file sent + * (e.g. Image001.jpg). For text/SMIL parts, the filename is generated by the + * sending phone and can usually be ignored. + */ + public void setName(String name) + { + this.name = name; + } + + public String getName() + { + return name; + } + + /* + * The text is the content of text-based MMS parts (application/smil, + * text/plain, or text/html), and is null for multimedia parts. + */ + public void setText(String text) + { + this.text = text; + } + + public String getText() + { + return text; + } + + /* + * For multimedia parts, the _data column of the MMS Parts table contains the + * path on the Android filesystem containing that file, and openInputStream + * returns an InputStream for this file. + */ + public InputStream openInputStream() throws FileNotFoundException + { + return app.getContentResolver().openInputStream(getContentUri()); + } + + + @Override + public String toString() + { + return "part " + partId + ": " + contentType + "; name=" + name + "; cid=" + cid; + } + + public Uri getContentUri() + { + return Uri.parse("content://mms/part/" + partId); + } + +} diff --git a/src/org/envaya/kalsms/MmsUtils.java b/src/org/envaya/kalsms/MmsUtils.java new file mode 100755 index 0000000..6027d84 --- /dev/null +++ b/src/org/envaya/kalsms/MmsUtils.java @@ -0,0 +1,176 @@ + +package org.envaya.kalsms; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/* + * Utilities for parsing IncomingMms from the MMS content provider tables, + * as defined by android.provider.Telephony + * + * Analogous to com.google.android.mms.pdu.PduPersister from + * core/java/com/google/android/mms/pdu in the base Android framework + * (https://github.com/android/platform_frameworks_base) + */ +public class MmsUtils +{ + // constants from android.provider.Telephony + public static final Uri OBSERVER_URI = Uri.parse("content://mms-sms/"); + public static final Uri INBOX_URI = Uri.parse("content://mms/inbox"); + public static final Uri PART_URI = Uri.parse("content://mms/part"); + + // constants from com.google.android.mms.pdu.PduHeaders + private static final int PDU_HEADER_FROM = 0x89; + private static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84; + + // todo -- prevent unbounded growth? + private final Set seenMmsContentLocations = new HashSet(); + + private App app; + private ContentResolver contentResolver; + + public MmsUtils(App app) + { + this.app = app; + this.contentResolver = app.getContentResolver(); + } + + private List getMmsParts(long id) + { + Cursor cur = contentResolver.query(PART_URI, new String[] { + "_id", "ct", "name", "text", "cid" + }, "mid = ?", new String[] { "" + id }, null); + + // assume that if there is at least one part saved in database + // then MMS is fully delivered (this seems to be true in practice) + + List parts = new ArrayList(); + + while (cur.moveToNext()) + { + long partId = cur.getLong(0); + + MmsPart part = new MmsPart(app, partId); + part.setContentType(cur.getString(1)); + part.setName(cur.getString(2)); + + // todo interpret charset like com.google.android.mms.pdu.EncodedStringValue + part.setText(cur.getString(3)); + + part.setContentId(cur.getString(4)); + + parts.add(part); + } + + cur.close(); + + return parts; + } + + /* + * see com.google.android.mms.pdu.PduPersister.loadAddress + */ + private String getSenderNumber(long mmsId) { + + Uri uri = Uri.parse("content://mms/"+mmsId+"/addr"); + + Cursor cur = contentResolver.query(uri, + new String[] { "address", "charset", "type" }, + null, null, null); + + String address = null; + while (cur.moveToNext()) + { + int addrType = cur.getInt(2); + if (addrType == PDU_HEADER_FROM) + { + // todo interpret charset like com.google.android.mms.pdu.EncodedStringValue + address = cur.getString(0); + } + } + cur.close(); + + return address; + } + + public List getMessagesInInbox() + { + // the M-Retrieve.conf messages are the 'actual' MMS messages + String m_type = "" + MESSAGE_TYPE_RETRIEVE_CONF; + + Cursor c = contentResolver.query(INBOX_URI, + new String[] {"_id", "ct_l"}, + "m_type = ? AND ct_l is not NULL", new String[] { m_type }, null); + + List messages = new ArrayList(); + + while (c.moveToNext()) + { + long id = c.getLong(0); + + IncomingMms mms = new IncomingMms(app, getSenderNumber(id), id); + + mms.setContentLocation(c.getString(1)); + + for (MmsPart part : getMmsParts(id)) + { + mms.addPart(part); + } + + messages.add(mms); + } + c.close(); + + return messages; + } + + public boolean deleteFromInbox(IncomingMms mms) + { + String contentLocation = mms.getContentLocation(); + + int res; + if (contentLocation != null) + { + Uri uri = Uri.parse("content://mms/inbox"); + + /* + * Delete by content location (ct_l) rather than _id so that + * M-Notification.ind and M-Retrieve.conf messages are both deleted + * (otherwise it would remain in Messaging inbox with a Download button) + */ + + res = contentResolver.delete(uri, + "ct_l = ?", + new String[] { contentLocation }); + } + else + { + app.log("mms has no content-location"); + Uri uri = Uri.parse("content://mms/inbox/" + mms.getId()); + res = contentResolver.delete(uri, null, null); + } + + app.log(res + " rows deleted"); + return res > 0; + } + + public synchronized void markOldMms(IncomingMms mms) + { + String contentLocation = mms.getContentLocation(); + if (contentLocation != null) + { + seenMmsContentLocations.add(contentLocation); + } + } + + public synchronized boolean isNewMms(IncomingMms mms) + { + String contentLocation = mms.getContentLocation(); + return contentLocation != null && !seenMmsContentLocations.contains(contentLocation); + } +} diff --git a/src/org/envaya/kalsms/OutgoingMessage.java b/src/org/envaya/kalsms/OutgoingMessage.java index 67ae0d1..98c5e4f 100755 --- a/src/org/envaya/kalsms/OutgoingMessage.java +++ b/src/org/envaya/kalsms/OutgoingMessage.java @@ -30,9 +30,9 @@ public class OutgoingMessage extends QueuedMessage { return nextLocalId++; } - public String getId() + public Uri getUri() { - return (serverId == null) ? localId : serverId; + return Uri.withAppendedPath(App.OUTGOING_URI, ((serverId == null) ? localId : serverId)); } public String getLogName() @@ -90,7 +90,7 @@ public class OutgoingMessage extends QueuedMessage { SmsManager smgr = SmsManager.getDefault(); Intent intent = new Intent(app, MessageStatusNotifier.class); - intent.setData(Uri.parse("kalsms://outgoing/" + getId())); + intent.setData(this.getUri()); PendingIntent sentIntent = PendingIntent.getBroadcast( app, @@ -103,7 +103,7 @@ public class OutgoingMessage extends QueuedMessage { protected Intent getRetryIntent() { Intent intent = new Intent(app, OutgoingMessageRetry.class); - intent.setData(Uri.parse("kalsms://outgoing/" + getId())); + intent.setData(this.getUri()); return intent; } } diff --git a/src/org/envaya/kalsms/QueuedMessage.java b/src/org/envaya/kalsms/QueuedMessage.java index 1f02c96..59915c4 100755 --- a/src/org/envaya/kalsms/QueuedMessage.java +++ b/src/org/envaya/kalsms/QueuedMessage.java @@ -4,6 +4,7 @@ import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.SystemClock; public abstract class QueuedMessage @@ -62,6 +63,8 @@ public abstract class QueuedMessage return true; } + + public abstract Uri getUri(); public abstract void retryNow(); diff --git a/src/org/envaya/kalsms/receiver/BootReceiver.java b/src/org/envaya/kalsms/receiver/BootReceiver.java index 4c3c527..f78327d 100755 --- a/src/org/envaya/kalsms/receiver/BootReceiver.java +++ b/src/org/envaya/kalsms/receiver/BootReceiver.java @@ -4,19 +4,12 @@ package org.envaya.kalsms.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import org.envaya.kalsms.App; public class BootReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - App app = (App)context.getApplicationContext(); - if (!app.isEnabled()) - { - return; - } - - app.setOutgoingMessageAlarm(); + // just want to initialize App class to start outgoing message poll timer } } diff --git a/src/org/envaya/kalsms/receiver/IncomingMessageRetry.java b/src/org/envaya/kalsms/receiver/IncomingMessageRetry.java index ad1a91b..08bfad5 100755 --- a/src/org/envaya/kalsms/receiver/IncomingMessageRetry.java +++ b/src/org/envaya/kalsms/receiver/IncomingMessageRetry.java @@ -12,6 +12,6 @@ public class IncomingMessageRetry extends BroadcastReceiver public void onReceive(Context context, Intent intent) { App app = (App) context.getApplicationContext(); - app.retryIncomingMessage(intent.getData().getLastPathSegment()); + app.retryIncomingMessage(intent.getData()); } } diff --git a/src/org/envaya/kalsms/receiver/MMSReceiver.java b/src/org/envaya/kalsms/receiver/MMSReceiver.java deleted file mode 100755 index a97e5a4..0000000 --- a/src/org/envaya/kalsms/receiver/MMSReceiver.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Based on http://code.google.com/p/android-notifier/, copyright 2011 Rodrigo Damazio - * Licensed under the Apache License, Version 2.0 - */ - -package org.envaya.kalsms.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import org.envaya.kalsms.App; - -public class MMSReceiver extends BroadcastReceiver { - - private App app; - - @Override - public void onReceive(Context context, Intent intent) { - app = (App) context.getApplicationContext(); - - if (!app.isEnabled()) { - return; - } - - app.log("WAP Push received"); - } -} \ No newline at end of file diff --git a/src/org/envaya/kalsms/receiver/MessageStatusNotifier.java b/src/org/envaya/kalsms/receiver/MessageStatusNotifier.java index ff31663..ca46363 100755 --- a/src/org/envaya/kalsms/receiver/MessageStatusNotifier.java +++ b/src/org/envaya/kalsms/receiver/MessageStatusNotifier.java @@ -7,6 +7,7 @@ package org.envaya.kalsms.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.net.Uri; import org.envaya.kalsms.App; public class MessageStatusNotifier extends BroadcastReceiver { @@ -14,7 +15,7 @@ public class MessageStatusNotifier extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { App app = (App) context.getApplicationContext(); - String id = intent.getData().getLastPathSegment(); + Uri uri = intent.getData(); int resultCode = getResultCode(); @@ -26,6 +27,6 @@ public class MessageStatusNotifier extends BroadcastReceiver { } */ - app.notifyOutgoingMessageStatus(id, resultCode); + app.notifyOutgoingMessageStatus(uri, resultCode); } } diff --git a/src/org/envaya/kalsms/receiver/OutgoingMessageRetry.java b/src/org/envaya/kalsms/receiver/OutgoingMessageRetry.java index ed48c46..8abc431 100755 --- a/src/org/envaya/kalsms/receiver/OutgoingMessageRetry.java +++ b/src/org/envaya/kalsms/receiver/OutgoingMessageRetry.java @@ -12,6 +12,6 @@ public class OutgoingMessageRetry extends BroadcastReceiver public void onReceive(Context context, Intent intent) { App app = (App) context.getApplicationContext(); - app.retryOutgoingMessage(intent.getData().getLastPathSegment()); + app.retryOutgoingMessage(intent.getData()); } } diff --git a/src/org/envaya/kalsms/receiver/SMSReceiver.java b/src/org/envaya/kalsms/receiver/SMSReceiver.java index faf9e06..d95ba83 100755 --- a/src/org/envaya/kalsms/receiver/SMSReceiver.java +++ b/src/org/envaya/kalsms/receiver/SMSReceiver.java @@ -9,9 +9,10 @@ import java.util.ArrayList; import java.util.List; import org.envaya.kalsms.App; import org.envaya.kalsms.IncomingMessage; +import org.envaya.kalsms.IncomingSms; -public class SMSReceiver extends BroadcastReceiver { +public class SmsReceiver extends BroadcastReceiver { private App app; @@ -64,7 +65,7 @@ public class SMSReceiver extends BroadcastReceiver { for (Object pdu : (Object[]) bundle.get("pdus")) { SmsMessage sms = SmsMessage.createFromPdu((byte[]) pdu); - messages.add(new IncomingMessage(app, sms)); + messages.add(new IncomingSms(app, sms)); } return messages; } diff --git a/src/org/envaya/kalsms/task/ForwarderTask.java b/src/org/envaya/kalsms/task/ForwarderTask.java index cdb473c..ccff314 100755 --- a/src/org/envaya/kalsms/task/ForwarderTask.java +++ b/src/org/envaya/kalsms/task/ForwarderTask.java @@ -16,7 +16,7 @@ public class ForwarderTask extends HttpTask { params.add(new BasicNameValuePair("action", App.ACTION_INCOMING)); } - + @Override protected String getDefaultToAddress() { return originalSms.getFrom(); diff --git a/src/org/envaya/kalsms/task/HttpTask.java b/src/org/envaya/kalsms/task/HttpTask.java index c922c54..acb50a3 100755 --- a/src/org/envaya/kalsms/task/HttpTask.java +++ b/src/org/envaya/kalsms/task/HttpTask.java @@ -14,7 +14,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; @@ -22,6 +24,11 @@ import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntity; +import org.apache.http.entity.mime.content.ContentBody; +import org.apache.http.entity.mime.content.StringBody; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; @@ -42,6 +49,9 @@ public class HttpTask extends AsyncTask { protected String url; protected List params = new ArrayList(); + + private List formParts; + private boolean useMultipartPost = false; public HttpTask(App app, BasicNameValuePair... paramsArr) { @@ -52,7 +62,13 @@ public class HttpTask extends AsyncTask { params.add(new BasicNameValuePair("version", "2")); params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber())); } - + + public void setFormParts(List formParts) + { + useMultipartPost = true; + this.formParts = formParts; + } + public HttpClient getHttpClient() { HttpParams httpParameters = new BasicHttpParams(); @@ -102,10 +118,29 @@ public class HttpTask extends AsyncTask { } HttpPost post = new HttpPost(url); - - post.setEntity(new UrlEncodedFormEntity(params)); + + + if (useMultipartPost) + { + MultipartEntity entity = new MultipartEntity();//HttpMultipartMode.BROWSER_COMPATIBLE); + + for (BasicNameValuePair param : params) + { + entity.addPart(param.getName(), new StringBody(param.getValue())); + } + + for (FormBodyPart formPart : formParts) + { + entity.addPart(formPart); + } + post.setEntity(entity); + } + else + { + post.setEntity(new UrlEncodedFormEntity(params)); + } - String signature = getSignature(); + String signature = getSignature(); post.setHeader("X-Kalsms-Signature", signature); diff --git a/src/org/envaya/kalsms/ui/ForwardInbox.java b/src/org/envaya/kalsms/ui/ForwardInbox.java index b9d96af..ce74d8a 100755 --- a/src/org/envaya/kalsms/ui/ForwardInbox.java +++ b/src/org/envaya/kalsms/ui/ForwardInbox.java @@ -11,6 +11,7 @@ import android.widget.ListView; import android.widget.SimpleCursorAdapter; import org.envaya.kalsms.App; import org.envaya.kalsms.IncomingMessage; +import org.envaya.kalsms.IncomingSms; import org.envaya.kalsms.R; @@ -74,7 +75,7 @@ public class ForwardInbox extends ListActivity { String body = cur.getString(bodyIndex); long date = cur.getLong(dateIndex); - IncomingMessage sms = new IncomingMessage(app, address, body, date); + IncomingMessage sms = new IncomingSms(app, address, body, date); app.forwardToServer(sms); } diff --git a/src/org/envaya/kalsms/ui/Main.java b/src/org/envaya/kalsms/ui/Main.java index da3c1e7..fbee98a 100755 --- a/src/org/envaya/kalsms/ui/Main.java +++ b/src/org/envaya/kalsms/ui/Main.java @@ -3,9 +3,11 @@ package org.envaya.kalsms.ui; import org.envaya.kalsms.task.HttpTask; import android.app.Activity; import android.content.BroadcastReceiver; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Html; @@ -19,6 +21,8 @@ import android.widget.TextView; import org.apache.http.HttpResponse; import org.apache.http.message.BasicNameValuePair; import org.envaya.kalsms.App; +import org.envaya.kalsms.IncomingMms; +import org.envaya.kalsms.MmsUtils; import org.envaya.kalsms.R; public class Main extends Activity { @@ -45,8 +49,6 @@ public class Main extends Activity { app.log("Server connection OK!"); } } - - private long lastLogTime = 0; public void updateLogView() { @@ -81,14 +83,7 @@ public class Main extends Activity { if (savedInstanceState == null) { - app.log(Html.fromHtml( - app.isEnabled() ? "SMS gateway running." : "SMS gateway disabled.")); - - app.log("Server URL is: " + app.getDisplayString(app.getServerUrl())); - app.log("Your phone number is: " + app.getDisplayString(app.getPhoneNumber()) ); app.log(Html.fromHtml("Press Menu to edit settings.")); - - app.setOutgoingMessageAlarm(); } } @@ -99,7 +94,7 @@ public class Main extends Activity { case R.id.settings: startActivity(new Intent(this, Prefs.class)); return true; - case R.id.check_now: + case R.id.check_now: app.checkOutgoingMessages(); return true; case R.id.retry_now: