FacebookTwitterGoogle+Share

An SMS relay for android

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 onReceive function gets called when a text message is received. Apparently, multipart SMSs are contained within the same intent – which is really useful! In certain cases this seems to be not true. I haven’t inspected the hex for these messages, but sometimes I’ll receive ones that start with something like “(1/3)”. I strongly suspect that the phone sending those messages is sending 3 individual texts, and prepending the “([sequence number]/[total parts])” to the message string. I’m not sure why this is the case.

The logic behind the onReceive function:

  1. Check if it’s an SMS_RECEIVED action, and make sure there’s content.
  2. 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.)
  3. 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.
  4. Figure out the name of the sender by looking up the number the SMS was received from in the contacts list (see: getOriginator(..))
  5. Send a toast: display a temporary message
  6. Relay the SMS
  7. 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

 

Comments