I made a simple SMS relay for my android (Andrew) last night. In case you want to do something similar, good news I’m about to explain its code!
Note: I used NME and notepad++ and flashdevelop and java, instead of NME and haXe, or Eclipse and Java. This might strike you as strange. It probably is strange, but it’s easier and more comfortable for me. The code shown will mostly be java and XML, which should match up nicely if you’re using Eclipse and Java, and you can take a look at Programming for android in Java but using NME if you’re using NME.
Let’s start with AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="::ANDROID_INSTALL_LOCATION::" android:versionCode="1" android:versionName="1.0" package="::APP_PACKAGE::">
<application android:label="::APP_TITLE::" android:debuggable="true"::if (HAS_ICON):: android:icon="@drawable/icon"::end::>
<receiver android:name=".TextMessageReceiver" >
<intent-filter android:priority="999">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<service android:name=".SMSService" />
</application>
<uses-sdk android:minSdkVersion="7"/>
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.WRITE_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
</manifest>
All these ::ANDROID_INSTALL_LOCATION::s, and the like, floating around around are because I copied over NME’s AndroidManifest.xml and modified it. NME fills those things in, presumably using application.nmml. They can probably be replaced by whatever you’d normally put in an AndroidManifest if you’re using eclipse.
The important parts are the receiver block, and the permissions. TextMessageReceiver is my BroadcastReceiver class.
Honestly, I don’t know what the difference is between android.permission.WRITE_SMS and android.permission.SEND_SMS and I didn’t find the docs particularly informative, so I included both.
Now comes the code! In comgigglingcorpsesms_relayTextMessageReceiver.java:
(Various pieces of this code were taken from random places on the internet and modified)
package com.gigglingcorpse.sms_relay;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.SmsMessage;
import android.provider.ContactsContract;
import android.database.Cursor;
import android.net.Uri;
import android.widget.Toast;
import android.app.Notification;
import android.app.NotificationManager;
import java.util.HashMap;
import java.util.regex.*;
import android.util.Log;
import android.telephony.SmsManager;
import android.app.PendingIntent;
import java.util.ArrayList;
public class TextMessageReceiver extends BroadcastReceiver{
static final String ACTION = "android.provider.Telephony.SMS_RECEIVED";
// Copied these off the internet and adjusted them
public static final String SMS_ADDRESS_PARAM="SMS_ADDRESS_PARAM";
public static final String SMS_DELIVERY_MSG_PARAM="SMS_DELIVERY_MSG_PARAM";
public static final String SMS_SENT_ACTION="com.gigglingcorpse.sms_relay.SMS_SENT";
private static final int RELAY_ID = 1; // I just picked 1
private HashMap hash; // This will hold our key->number hash
public TextMessageReceiver() {
super();
hash = new HashMap();
// Fill the hash with values
hash.put("str", "1234567890");
}
public void onReceive(Context context, Intent intent)
{
if( intent.getAction().equals(ACTION) ) {
Bundle bundle=intent.getExtras();
if ( bundle != null ) {
// Handle multi-part messages
HashMap completeMsgs = new HashMap();
// Create a list of SMS messages
Object[] messages=(Object[])bundle.get("pdus");
SmsMessage[] sms=new SmsMessage[messages.length];
for(int n=0;n<messages.length;n++){
sms[n]=SmsMessage.createFromPdu((byte[]) messages[n]);
}
// Join multi-part messages
for(SmsMessage msg:sms){
String sKey = msg.getOriginatingAddress(); // Where the message came from
String message = msg.getMessageBody();
// Append this message if there was an earlier one by the same sender
String old = "";
if ( completeMsgs.containsKey( sKey ) == true ) {
old = completeMsgs.get( sKey ).toString();
}
completeMsgs.put( sKey, old + message );
}
// For each complete message
for( Object sKey:completeMsgs.keySet()){
String message = completeMsgs.get(sKey).toString();
// Check if the message should be forwarded
Pattern p = Pattern.compile("^\@([^:]+):(.*)", Pattern.DOTALL);
Matcher m = p.matcher( message );
if ( m.find() == true ) {
String key = m.group(1).toLowerCase();
// If it's a valid recipient
if ( hash.containsKey( key ) ) {
String from = getOriginator( context, sKey.toString() );
// Append their name to the message
String newMessage = from + ':' + m.group(2);
// Display summary on my phone
String summary = "Attempting to relay from " + from + " to " + key + "(" + hash.get(key) +")";
display( context, summary );
relay( context, hash.get( key ).toString(), newMessage );
// Don't send the message to my inbox
abortBroadcast();
}
}
}
}
}
}
/**
* Relay the message.
*/
private void relay( Context c, String destination, String message ) {
SmsManager smsMgr = SmsManager.getDefault();
ArrayList<String> messages = smsMgr.divideMessage(message);
ArrayList<PendingIntent> listOfIntents = new ArrayList<PendingIntent>();
for (int i=0; i < messages.size(); i++){
Intent sentIntent = new Intent(SMS_SENT_ACTION);
sentIntent.putExtra(SMS_ADDRESS_PARAM, destination);
sentIntent.putExtra(SMS_DELIVERY_MSG_PARAM, (messages.size() > 1)? "Part " + i + " of SMS " : "SMS ");
PendingIntent pi = PendingIntent.getBroadcast(c, 0, sentIntent, PendingIntent.FLAG_CANCEL_CURRENT);
listOfIntents.add(pi);
}
smsMgr.sendMultipartTextMessage(destination, null, messages, listOfIntents, null);
}
/**
* Send a toast notifying me of a relay.
*/
private void display( Context c, String s ) {
Toast t = Toast.makeText(c, s, Toast.LENGTH_LONG);
t.show();
}
/**
* Look up a contact name from an originator string.
* @param Context context.
* @param String originator in most cases a phone number.
* @return String a name or the originator string if it couldn't find a suitable contact.
*/
private String getOriginator( Context context, String originator ) {
Uri uri;
String[] projection;
String fromDisplayName = originator;
uri = Uri.withAppendedPath(
ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
Uri.encode(fromDisplayName));
projection = new String[] { ContactsContract.PhoneLookup.DISPLAY_NAME };
// Query the filter URI
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst())
fromDisplayName = cursor.getString(0);
cursor.close();
}
return fromDisplayName;
}
}
Wow, that formatting is just terrible. I apologize. I wonder if there’s an easy way to turn down tab length.. Oh, well.
That’s it, basically! Hopefully the in-code comments are enough, because I can’t really think of much to discuss about it. One thing is the first two loops in the onReceive could very easily be combined into one. I was being lazy.
The
The logic behind the onReceive function:
- Check if it’s an SMS_RECEIVED action, and make sure there’s content.
- Combine the message strings for each part in the Intent. This is so we have one complete message. (I don’t think the completeMsgs HashMap stuff is necessary. As far as I can tell, separate texts come in as separate Intents. I left it in there out of laziness.)
- Check if the SMS should be relayed. The regex used checks for “@key:msg here“. Apparently, . (dot) doesn’t include end-of-line type characters, of which n is one. That is why I’ve included the Pattern.DOTALL flag.
- Figure out the name of the sender by looking up the number the SMS was received from in the contacts list (see: getOriginator(..))
- Send a toast: display a temporary message
- Relay the SMS
- Abort the broadcast, so that the message doesn’t get to the program that would save it to my inbox.
Since the new message could very well be longer than the old message (subtract the key, and add the contact name or number), in relay(..) we redivide the message before sending it.
And that’s all there is to the code!
Since then, I’ve added the sender’s number to the message for certain hash entries, and written another program to go on the receiving phone. It checks for the number, and swaps out the real sender with the one the message contains. This allows you to relay through the one phone, but have the message show up in the correct conversation on the receiving phone.
The end,
Brad