5
0
mirror of https://github.com/cwinfo/envayasms.git synced 2024-09-18 21:39:34 +00:00

support for incoming MMS

This commit is contained in:
Jesse Young 2011-09-17 18:38:16 -07:00
parent 60d49414e1
commit d994b10c35
23 changed files with 787 additions and 159 deletions

View File

@ -33,19 +33,12 @@
android:label="@string/app_name"> android:label="@string/app_name">
</activity> </activity>
<receiver android:name=".receiver.SMSReceiver"> <receiver android:name=".receiver.SmsReceiver">
<intent-filter android:priority="101"> <intent-filter android:priority="101">
<action android:name="android.provider.Telephony.SMS_RECEIVED" /> <action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".receiver.MMSReceiver">
<intent-filter android:priority="101">
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED" />
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.MessageStatusNotifier"> <receiver android:name=".receiver.MessageStatusNotifier">
</receiver> </receiver>
@ -63,5 +56,8 @@
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<service android:name=".CheckMmsInboxService">
</service>
</application> </application>
</manifest> </manifest>

BIN
libs/httpmime-4.1.2.jar Executable file

Binary file not shown.

View File

@ -41,5 +41,6 @@
android:title="Keep new messages?" android:title="Keep new messages?"
android:summaryOff="Incoming SMS will not be stored in Messaging inbox" android:summaryOff="Incoming SMS will not be stored in Messaging inbox"
android:summaryOn="Incoming SMS will be stored in Messaging inbox" android:summaryOn="Incoming SMS will be stored in Messaging inbox"
></CheckBoxPreference> ></CheckBoxPreference>
</PreferenceScreen> </PreferenceScreen>

View File

@ -10,9 +10,11 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri;
import android.os.SystemClock; import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.telephony.SmsManager; import android.telephony.SmsManager;
import android.text.Html;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.util.Log; import android.util.Log;
import java.text.DateFormat; 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_OUTGOING = "outgoing";
public static final String ACTION_INCOMING = "incoming"; public static final String ACTION_INCOMING = "incoming";
public static final String ACTION_SEND_STATUS = "send_status"; public static final String ACTION_SEND_STATUS = "send_status";
public static final String STATUS_QUEUED = "queued"; public static final String STATUS_QUEUED = "queued";
public static final String STATUS_FAILED = "failed"; public static final String STATUS_FAILED = "failed";
public static final String STATUS_SENT = "sent"; 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_NAME = "KALSMS";
public static final String LOG_INTENT = "org.envaya.kalsms.LOG"; public static final String LOG_INTENT = "org.envaya.kalsms.LOG";
public static final int MAX_DISPLAYED_LOG = 15000; public static final int MAX_DISPLAYED_LOG = 15000;
public static final int LOG_TIMESTAMP_INTERVAL = 60000; public static final int LOG_TIMESTAMP_INTERVAL = 60000;
private long lastLogTime = 0; // Each QueuedMessage is identified within our internal Map by its Uri.
private SpannableStringBuilder displayedLog = new SpannableStringBuilder(); // Currently QueuedMessage instances are only available within KalSMS,
private Map<String, IncomingMessage> incomingSmsMap = new HashMap<String, IncomingMessage>(); // (but they could be made available to other applications later via a ContentProvider)
private Map<String, OutgoingMessage> outgoingSmsMap = new HashMap<String, OutgoingMessage>(); 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<Uri, IncomingMessage> incomingMessages = new HashMap<Uri, IncomingMessage>();
private Map<Uri, OutgoingMessage> outgoingMessages = new HashMap<Uri, OutgoingMessage>();
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() ? "<b>SMS gateway running.</b>" : "<b>SMS gateway disabled.</b>"));
log("Server URL is: " + getDisplayString(getServerUrl()));
log("Your phone number is: " + getDisplayString(getPhoneNumber()));
mmsObserver = new MmsObserver(this);
mmsObserver.register();
setOutgoingMessageAlarm();
}
public void checkOutgoingMessages() public void checkOutgoingMessages()
{ {
@ -92,29 +126,29 @@ public final class App extends Application {
} }
public String getServerUrl() { public String getServerUrl() {
return getSettings().getString("server_url", ""); return settings.getString("server_url", "");
} }
public String getPhoneNumber() { public String getPhoneNumber() {
return getSettings().getString("phone_number", ""); return settings.getString("phone_number", "");
} }
public int getOutgoingPollSeconds() { public int getOutgoingPollSeconds() {
return Integer.parseInt(getSettings().getString("outgoing_interval", "0")); return Integer.parseInt(settings.getString("outgoing_interval", "0"));
} }
public boolean isEnabled() public boolean isEnabled()
{ {
return getSettings().getBoolean("enabled", false); return settings.getBoolean("enabled", false);
} }
public boolean getKeepInInbox() public boolean getKeepInInbox()
{ {
return getSettings().getBoolean("keep_in_inbox", false); return settings.getBoolean("keep_in_inbox", false);
} }
public String getPassword() { public String getPassword() {
return getSettings().getString("password", ""); return settings.getString("password", "");
} }
private void notifyStatus(OutgoingMessage sms, String status, String errorMessage) { private void notifyStatus(OutgoingMessage sms, String status, String errorMessage) {
@ -150,35 +184,45 @@ public final class App extends Application {
} }
public synchronized int getStuckMessageCount() { public synchronized int getStuckMessageCount() {
return outgoingSmsMap.size() + incomingSmsMap.size(); return outgoingMessages.size() + incomingMessages.size();
} }
public synchronized void retryStuckOutgoingMessages() { public synchronized void retryStuckOutgoingMessages() {
for (OutgoingMessage sms : outgoingSmsMap.values()) { for (OutgoingMessage sms : outgoingMessages.values()) {
sms.retryNow(); sms.retryNow();
} }
} }
public synchronized void retryStuckIncomingMessages() { public synchronized void retryStuckIncomingMessages() {
for (IncomingMessage sms : incomingSmsMap.values()) { for (IncomingMessage sms : incomingMessages.values()) {
sms.retryNow(); sms.retryNow();
} }
} }
public synchronized void setIncomingMessageStatus(IncomingMessage sms, boolean success) { public synchronized void setIncomingMessageStatus(IncomingMessage message, boolean success) {
String id = sms.getId(); Uri uri = message.getUri();
if (success) 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) { public synchronized void notifyOutgoingMessageStatus(Uri uri, int resultCode) {
OutgoingMessage sms = outgoingSmsMap.get(id); OutgoingMessage sms = outgoingMessages.get(uri);
if (sms == null) { if (sms == null) {
return; return;
@ -210,52 +254,52 @@ public final class App extends Application {
case SmsManager.RESULT_ERROR_RADIO_OFF: case SmsManager.RESULT_ERROR_RADIO_OFF:
case SmsManager.RESULT_ERROR_NO_SERVICE: case SmsManager.RESULT_ERROR_NO_SERVICE:
if (!sms.scheduleRetry()) { if (!sms.scheduleRetry()) {
outgoingSmsMap.remove(id); outgoingMessages.remove(uri);
} }
break; break;
default: default:
outgoingSmsMap.remove(id); outgoingMessages.remove(uri);
break; break;
} }
} }
public synchronized void sendOutgoingMessage(OutgoingMessage sms) { public synchronized void sendOutgoingMessage(OutgoingMessage sms) {
String id = sms.getId(); Uri uri = sms.getUri();
if (outgoingSmsMap.containsKey(id)) { if (outgoingMessages.containsKey(uri)) {
log(sms.getLogName() + " already sent, skipping"); log(sms.getLogName() + " already sent, skipping");
return; return;
} }
outgoingSmsMap.put(id, sms); outgoingMessages.put(uri, sms);
log("Sending " + sms.getLogName() + " to " + sms.getTo()); log("Sending " + sms.getLogName() + " to " + sms.getTo());
sms.trySend(); sms.trySend();
} }
public synchronized void forwardToServer(IncomingMessage sms) { public synchronized void forwardToServer(IncomingMessage message) {
String id = sms.getId(); Uri uri = message.getUri();
if (incomingSmsMap.containsKey(id)) { if (incomingMessages.containsKey(uri)) {
log("Duplicate incoming SMS, skipping"); log("Duplicate incoming "+message.getDisplayType()+", skipping");
return; 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) { public synchronized void retryIncomingMessage(Uri uri) {
IncomingMessage sms = incomingSmsMap.get(id); IncomingMessage message = incomingMessages.get(uri);
if (sms != null) { if (message != null) {
sms.retryNow(); message.retryNow();
} }
} }
public synchronized void retryOutgoingMessage(String id) { public synchronized void retryOutgoingMessage(Uri uri) {
OutgoingMessage sms = outgoingSmsMap.get(id); OutgoingMessage sms = outgoingMessages.get(uri);
if (sms != null) { if (sms != null) {
sms.retryNow(); sms.retryNow();
} }
@ -265,7 +309,7 @@ public final class App extends Application {
Log.d(LOG_NAME, msg); Log.d(LOG_NAME, msg);
} }
public void log(CharSequence msg) public synchronized void log(CharSequence msg)
{ {
Log.d(LOG_NAME, msg.toString()); Log.d(LOG_NAME, msg.toString());
@ -303,7 +347,7 @@ public final class App extends Application {
sendBroadcast(broadcast); sendBroadcast(broadcast);
} }
public CharSequence getDisplayedLog() public synchronized CharSequence getDisplayedLog()
{ {
return displayedLog; return displayedLog;
} }
@ -328,6 +372,10 @@ public final class App extends Application {
logError("Inner exception:", innerEx, true); logError("Inner exception:", innerEx, true);
} }
} }
}
public MmsUtils getMmsUtils()
{
return mmsUtils;
} }
} }

View File

@ -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<IncomingMms> 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);
}
}
}
}

View File

@ -1,32 +1,21 @@
package org.envaya.kalsms; package org.envaya.kalsms;
import org.envaya.kalsms.task.ForwarderTask;
import org.envaya.kalsms.receiver.IncomingMessageRetry;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.telephony.SmsMessage; import org.envaya.kalsms.receiver.IncomingMessageRetry;
import org.apache.http.message.BasicNameValuePair;
public class IncomingMessage extends QueuedMessage { public abstract class IncomingMessage extends QueuedMessage {
public String from; protected 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();
}
public IncomingMessage(App app, String from, String message, long timestampMillis) { public IncomingMessage(App app, String from)
{
super(app); super(app);
this.from = from; this.from = from;
this.message = message; }
this.timestampMillis = timestampMillis;
} public abstract String getDisplayType();
public boolean isForwardable() public boolean isForwardable()
{ {
/* /*
@ -36,37 +25,21 @@ public class IncomingMessage extends QueuedMessage {
return from.length() > 5; return from.length() > 5;
} }
public String getMessageBody()
{
return message;
}
public String getFrom() public String getFrom()
{ {
return from; return from;
} }
public String getId()
{
return from + ":" + message + ":" + timestampMillis;
}
public void retryNow() { public void retryNow() {
app.log("Retrying forwarding SMS from " + from); app.log("Retrying forwarding message from " + from);
tryForwardToServer(); tryForwardToServer();
} }
public void tryForwardToServer() {
new ForwarderTask(this,
new BasicNameValuePair("from", getFrom()),
new BasicNameValuePair("message", getMessageBody())
).execute();
}
protected Intent getRetryIntent() { protected Intent getRetryIntent() {
Intent intent = new Intent(app, IncomingMessageRetry.class); Intent intent = new Intent(app, IncomingMessageRetry.class);
intent.setData(Uri.parse("kalsms://incoming/" + this.getId())); intent.setData(this.getUri());
return intent; return intent;
} }
public abstract void tryForwardToServer();
} }

View File

@ -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<MmsPart> parts;
long id;
String contentLocation;
public IncomingMms(App app, String from, long id)
{
super(app, from);
this.parts = new ArrayList<MmsPart>();
this.id = id;
}
public String getDisplayType()
{
return "MMS";
}
public List<MmsPart> 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<FormBodyPart> formParts = new ArrayList<FormBodyPart>();
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);
}
}

View File

@ -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();
}
}

View File

@ -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<IncomingMms> 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));
}
}
}

View File

@ -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 <img region="Image" src="cid:805"/> . 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);
}
}

View File

@ -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<String> seenMmsContentLocations = new HashSet<String>();
private App app;
private ContentResolver contentResolver;
public MmsUtils(App app)
{
this.app = app;
this.contentResolver = app.getContentResolver();
}
private List<MmsPart> 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<MmsPart> parts = new ArrayList<MmsPart>();
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<IncomingMms> 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<IncomingMms> messages = new ArrayList<IncomingMms>();
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);
}
}

View File

@ -30,9 +30,9 @@ public class OutgoingMessage extends QueuedMessage {
return nextLocalId++; 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() public String getLogName()
@ -90,7 +90,7 @@ public class OutgoingMessage extends QueuedMessage {
SmsManager smgr = SmsManager.getDefault(); SmsManager smgr = SmsManager.getDefault();
Intent intent = new Intent(app, MessageStatusNotifier.class); Intent intent = new Intent(app, MessageStatusNotifier.class);
intent.setData(Uri.parse("kalsms://outgoing/" + getId())); intent.setData(this.getUri());
PendingIntent sentIntent = PendingIntent.getBroadcast( PendingIntent sentIntent = PendingIntent.getBroadcast(
app, app,
@ -103,7 +103,7 @@ public class OutgoingMessage extends QueuedMessage {
protected Intent getRetryIntent() { protected Intent getRetryIntent() {
Intent intent = new Intent(app, OutgoingMessageRetry.class); Intent intent = new Intent(app, OutgoingMessageRetry.class);
intent.setData(Uri.parse("kalsms://outgoing/" + getId())); intent.setData(this.getUri());
return intent; return intent;
} }
} }

View File

@ -4,6 +4,7 @@ import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock; import android.os.SystemClock;
public abstract class QueuedMessage public abstract class QueuedMessage
@ -62,6 +63,8 @@ public abstract class QueuedMessage
return true; return true;
} }
public abstract Uri getUri();
public abstract void retryNow(); public abstract void retryNow();

View File

@ -4,19 +4,12 @@ package org.envaya.kalsms.receiver;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import org.envaya.kalsms.App;
public class BootReceiver extends BroadcastReceiver { public class BootReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) public void onReceive(Context context, Intent intent)
{ {
App app = (App)context.getApplicationContext(); // just want to initialize App class to start outgoing message poll timer
if (!app.isEnabled())
{
return;
}
app.setOutgoingMessageAlarm();
} }
} }

View File

@ -12,6 +12,6 @@ public class IncomingMessageRetry extends BroadcastReceiver
public void onReceive(Context context, Intent intent) public void onReceive(Context context, Intent intent)
{ {
App app = (App) context.getApplicationContext(); App app = (App) context.getApplicationContext();
app.retryIncomingMessage(intent.getData().getLastPathSegment()); app.retryIncomingMessage(intent.getData());
} }
} }

View File

@ -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");
}
}

View File

@ -7,6 +7,7 @@ package org.envaya.kalsms.receiver;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import org.envaya.kalsms.App; import org.envaya.kalsms.App;
public class MessageStatusNotifier extends BroadcastReceiver { public class MessageStatusNotifier extends BroadcastReceiver {
@ -14,7 +15,7 @@ public class MessageStatusNotifier extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
App app = (App) context.getApplicationContext(); App app = (App) context.getApplicationContext();
String id = intent.getData().getLastPathSegment(); Uri uri = intent.getData();
int resultCode = getResultCode(); int resultCode = getResultCode();
@ -26,6 +27,6 @@ public class MessageStatusNotifier extends BroadcastReceiver {
} }
*/ */
app.notifyOutgoingMessageStatus(id, resultCode); app.notifyOutgoingMessageStatus(uri, resultCode);
} }
} }

View File

@ -12,6 +12,6 @@ public class OutgoingMessageRetry extends BroadcastReceiver
public void onReceive(Context context, Intent intent) public void onReceive(Context context, Intent intent)
{ {
App app = (App) context.getApplicationContext(); App app = (App) context.getApplicationContext();
app.retryOutgoingMessage(intent.getData().getLastPathSegment()); app.retryOutgoingMessage(intent.getData());
} }
} }

View File

@ -9,9 +9,10 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.envaya.kalsms.App; import org.envaya.kalsms.App;
import org.envaya.kalsms.IncomingMessage; import org.envaya.kalsms.IncomingMessage;
import org.envaya.kalsms.IncomingSms;
public class SMSReceiver extends BroadcastReceiver { public class SmsReceiver extends BroadcastReceiver {
private App app; private App app;
@ -64,7 +65,7 @@ public class SMSReceiver extends BroadcastReceiver {
for (Object pdu : (Object[]) bundle.get("pdus")) for (Object pdu : (Object[]) bundle.get("pdus"))
{ {
SmsMessage sms = SmsMessage.createFromPdu((byte[]) pdu); SmsMessage sms = SmsMessage.createFromPdu((byte[]) pdu);
messages.add(new IncomingMessage(app, sms)); messages.add(new IncomingSms(app, sms));
} }
return messages; return messages;
} }

View File

@ -16,7 +16,7 @@ public class ForwarderTask extends HttpTask {
params.add(new BasicNameValuePair("action", App.ACTION_INCOMING)); params.add(new BasicNameValuePair("action", App.ACTION_INCOMING));
} }
@Override @Override
protected String getDefaultToAddress() { protected String getDefaultToAddress() {
return originalSms.getFrom(); return originalSms.getFrom();

View File

@ -14,7 +14,9 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; 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.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost; 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.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams; import org.apache.http.params.BasicHttpParams;
@ -42,6 +49,9 @@ public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
protected String url; protected String url;
protected List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(); protected List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
private List<FormBodyPart> formParts;
private boolean useMultipartPost = false;
public HttpTask(App app, BasicNameValuePair... paramsArr) public HttpTask(App app, BasicNameValuePair... paramsArr)
{ {
@ -52,7 +62,13 @@ public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
params.add(new BasicNameValuePair("version", "2")); params.add(new BasicNameValuePair("version", "2"));
params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber())); params.add(new BasicNameValuePair("phone_number", app.getPhoneNumber()));
} }
public void setFormParts(List<FormBodyPart> formParts)
{
useMultipartPost = true;
this.formParts = formParts;
}
public HttpClient getHttpClient() public HttpClient getHttpClient()
{ {
HttpParams httpParameters = new BasicHttpParams(); HttpParams httpParameters = new BasicHttpParams();
@ -102,10 +118,29 @@ public class HttpTask extends AsyncTask<String, Void, HttpResponse> {
} }
HttpPost post = new HttpPost(url); 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); post.setHeader("X-Kalsms-Signature", signature);

View File

@ -11,6 +11,7 @@ import android.widget.ListView;
import android.widget.SimpleCursorAdapter; import android.widget.SimpleCursorAdapter;
import org.envaya.kalsms.App; import org.envaya.kalsms.App;
import org.envaya.kalsms.IncomingMessage; import org.envaya.kalsms.IncomingMessage;
import org.envaya.kalsms.IncomingSms;
import org.envaya.kalsms.R; import org.envaya.kalsms.R;
@ -74,7 +75,7 @@ public class ForwardInbox extends ListActivity {
String body = cur.getString(bodyIndex); String body = cur.getString(bodyIndex);
long date = cur.getLong(dateIndex); long date = cur.getLong(dateIndex);
IncomingMessage sms = new IncomingMessage(app, address, body, date); IncomingMessage sms = new IncomingSms(app, address, body, date);
app.forwardToServer(sms); app.forwardToServer(sms);
} }

View File

@ -3,9 +3,11 @@ package org.envaya.kalsms.ui;
import org.envaya.kalsms.task.HttpTask; import org.envaya.kalsms.task.HttpTask;
import android.app.Activity; import android.app.Activity;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.Html; import android.text.Html;
@ -19,6 +21,8 @@ import android.widget.TextView;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.envaya.kalsms.App; import org.envaya.kalsms.App;
import org.envaya.kalsms.IncomingMms;
import org.envaya.kalsms.MmsUtils;
import org.envaya.kalsms.R; import org.envaya.kalsms.R;
public class Main extends Activity { public class Main extends Activity {
@ -45,8 +49,6 @@ public class Main extends Activity {
app.log("Server connection OK!"); app.log("Server connection OK!");
} }
} }
private long lastLogTime = 0;
public void updateLogView() public void updateLogView()
{ {
@ -81,14 +83,7 @@ public class Main extends Activity {
if (savedInstanceState == null) if (savedInstanceState == null)
{ {
app.log(Html.fromHtml(
app.isEnabled() ? "<b>SMS gateway running.</b>" : "<b>SMS gateway disabled.</b>"));
app.log("Server URL is: " + app.getDisplayString(app.getServerUrl()));
app.log("Your phone number is: " + app.getDisplayString(app.getPhoneNumber()) );
app.log(Html.fromHtml("<b>Press Menu to edit settings.</b>")); app.log(Html.fromHtml("<b>Press Menu to edit settings.</b>"));
app.setOutgoingMessageAlarm();
} }
} }
@ -99,7 +94,7 @@ public class Main extends Activity {
case R.id.settings: case R.id.settings:
startActivity(new Intent(this, Prefs.class)); startActivity(new Intent(this, Prefs.class));
return true; return true;
case R.id.check_now: case R.id.check_now:
app.checkOutgoingMessages(); app.checkOutgoingMessages();
return true; return true;
case R.id.retry_now: case R.id.retry_now: