Putting Text to Speech to Work

Raise your (phone's) voice as we build out our Text To Speech app for Android.

Application for TTS

In a prior article we explored using the Text To Speech (TTS) capabilities native to Android.

In this article we begin to apply the TTS capabilities into an application that has (slightly) more utility.

The reasons for using Text To Speech range from the practical, safety-minded applications to the “just for fun”. The application we build in this article is arguably a little bit of both.

While our prior look at Text To Speech was geared around the mechanics of using the TTS features, this application spends a bit more time with the context of the application and leverages what we have previously learned.

Reading your messages

The name of the sample application is “Linux Magazine SMS Reader”, or LMSMSReader.

The application works as follows:

  • When an incoming text message arrives, our application is notified.
  • The application obtains a copy of the inbound message.
  • A new Activity is launched to “process” the message.
  • The “processing” of this message causes the TTS subsystem to “read” the message to us.
  • The incoming text message still winds up in the normal “inbox” as expected.

Before diving into the code, let’s enumerate from a high-level just what we’re going to need here:

  • A BroadcastReceiver class, to be notified of incoming SMS messages.
  • An Activity class to interact with the TTS services
  • Some “glue” to connect the BroadcastReceiver to the Activity.
  • An appropriately apportioned AndroidManifest.xml to wire everything up properly.

Let’s start by examining the code for our implementation of a BroadcastReceiver: LMSMSReader.java.

package com.navitend.lm.smsreader;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import android.os.Bundle;
import android.telephony.SmsMessage;

public class LMSMSReader extends BroadcastReceiver  {

	private final String tag = "LMSMSReader";

	@Override
	public void onReceive(Context context, Intent intent) {
		Log.i(tag,"onReceive invoked!");

		Bundle extras = intent.getExtras();
        if (extras == null) {
        	Log.i(tag,"didn't like this ....message");
        	return;
        }

        Object[] pdus = (Object[]) extras.get("pdus");
        Log.i(tag,"there are [" + pdus.length + "] messages");

        SmsMessage message = SmsMessage.createFromPdu((byte[]) pdus[0]);
        String fromAddress = message.getOriginatingAddress();

        Log.i(tag,"message from : " + fromAddress + " " + message.getMessageBody().toString());

        Intent rtmIntent = new Intent();
        rtmIntent.setClass(context, ReadThisMessage.class);
        rtmIntent.putExtra("MESSAGE", message.getMessageBody().toString());
        rtmIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |  Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
        context.startActivity(rtmIntent);
	}

}

A quick glance at this code reveals that the class LMSMSReader is extending the BroadcastReceiver class. You may recall that the BroadcastReceiver is one of the four main classes that can comprise an Android application. The others are: Actvity, Service, and ContentProvider.

A BroadcastReceiver’s onReceive method is invoked when a particular event occurs. In this case, we are interested in the scenario where a Text (or SMS) message has been received. When we look at the AndroidManifest.xml file in detail we will see how things are wired up. For now, know that the onReceive method of the LMSMSReader class is invoked when an SMS message is received.

The two arguments to the onReceive method include an application Context and an Intent. The Intent contains the information about our message. To obtain the message data itself, we need to extract the “extras” from the Intent with a call to the getExtras() method.

Assuming we don’t have a false alarm (i.e. no Extras), we extract the data labeled as “pdus”. PDU stands for “Protocol Description Unit”. You can learn more on the ‘net. A quick Google search brings up this site which provides some more specifics.

Multiple PDU’s may be found in an incoming message but for our purposes here, we’re just going to work with the first message.

We can extract some data including the sender’s address and the message body. You can learn more about the SMS message class by browsing the documentation.

Now that we’ve got a message, we would like the application to read it to us!

In order to accomplish this we need to package up the message contents into an Intent and then pass this on to our Activity which will take care of the TTS details for us.

We create a new Intent and explicitly tell it that we want to work with the ReadThisMessage class which we’ll review in a moment.

In addition to the class name, we also need to pass along the message contents which we do by adding an “Extra” with a name of “MESSAGE” and the value set to the body of the SMS Message.

The other tailoring we want to perform to our newly created Intent is to set some flags. There are many flags to choose from, so why these two?

A well-behaved BroadcastReceiver does it’s job of reacting to various notifications/stimuli, dispatching its work, and then getting out of the way. The job of this particular BroadcastReceiver implementation is simply to react to the incoming SMS and then pass off the data to our Activity. We want a new task to be created to provide allow the BroadcastReceiver to terminate and then let the newly created Activity do as it wishes so we set the flag named FLAG_ACTIVITY_NEW_TASK. In fact, if you try to call startActivity() without this flag, Android throws an exception.

The other flag we want to set has a little less obvious reason. If you press and hold the “Home” key on your Android device, you will see the most recently run applications. Some might be still running and others are just in the “most recently used”, or “MRU” list. This kind of functionality may or may not be what you want, so you can add or remove the FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS flag. If you want to be able to listen again to the most recently received Text Message, leave this flag out of the call to addFlags(). If you would prefer to not have the application listed in your “Recents”, then add the flag. Your choice.

Now it is time to look at the ReadThisMessage class implementation.

Reading the message

ReadThisMessage.java contains the code which interacts with the TTS service.

package com.navitend.lm.smsreader;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
import android.util.Log;
import java.util.HashMap;

public class ReadThisMessage extends Activity implements OnInitListener,OnUtteranceCompletedListener {

	private TextToSpeech tts = null;
	private final String tag = "LMSMSReader";
	private String msg = "";

	@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent startingIntent = this.getIntent();
        msg = startingIntent.getStringExtra("MESSAGE");
        tts = new TextToSpeech(this,this);
    }

    @Override
    protected void onDestroy() {
    	super.onDestroy();
    	Log.i(tag,"onDestroy");
    	if (tts!=null) {
    		tts.shutdown();
    	}
    }

    // OnInitListener impl
	public void onInit(int status) {
		Log.i(tag,"onInit");
		HashMap hm = new HashMap();
		hm.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "smsmessage");
		tts.setOnUtteranceCompletedListener(this);
		tts.speak(msg, TextToSpeech.QUEUE_FLUSH, hm);
	}

	// OnUtteranceCompletedListener impl
	public void onUtteranceCompleted(String utteranceId) {
		Log.i(tag,"onUtteranceCompleted :" + utteranceId);
		tts.shutdown();
		tts = null;
		finish();
	}
}

Note the import statements for the TextToSpeech classes. Also, we have imported the java.util.HashMap which is used to help track when the reading of our message is complete.

This Activity implements two listeners, one called OnInitListener and the other OnUtteranceCompletedListener.

The code for this Activity is fairly straight-forward though it does rely heavily upon the “callback” mechanism associated with the two listeners implemented.

In the onCreate method we extract the Intent that started the activity — we do this via a call to getIntent().

We then extract the “MESSAGE” parameter which is just a String and store it in a class-level variable named msg.

Next, we start-up the TTS functionality by creating an instance of the TextToSpeech class, passing in a Context which is satisfied by the “this” pointer which refers to the Activity. The next required parameter is a reference to a class which implements the OnInitListener. Again, “this” satisfies the requirement.

From here on out we are relying upon the callbacks. First, the onInit method is invoked when the TTS interface is ready.

In onInit() we setup a HashMap which is a simple reference to this message. Ideally we would use a unique identifier for each message to be spoken. In our case we simply pass in a value of “smsmessage” with a key value of TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID.

We tell this TextToSpeech object instance that we want to be notified when any utterances (i.e. speech) are completed. The key/value pair in the HashMap are used to differentiate between multiple utterances.

Then we ask for the speech to be uttered with a call to the speak() method, passing in the msg text, a parameter to flush any waiting phrases and finally our HashMap with the utterance id provided.

When the utterance has completed the onUtteranceCompleted method is invoked and we call the shutdown() method of the TextToSpeech object to properly “cleanup” and unbind our Activity from the TTS service.

In the case that this method is never invoked for whatever reason, we want to be a good Android-citizen and cleanup in the onDestroy method by conditionally calling the shutdown() method as necessary.

When an incoming message is received you should not only hear the message spoken, but also see some relevant action in the logcat.

04-02 17:37:48.374: INFO/LMSMSReader(7651): onReceive invoked!
04-02 17:37:48.374: INFO/LMSMSReader(7651): there are [1] messages
04-02 17:37:48.394: INFO/LMSMSReader(7651): message from : 19734480070What is for dinner?
04-02 17:37:48.674: INFO/LMSMSReader(7651): onInit
04-02 17:37:50.254: INFO/LMSMSReader(7651): onUtteranceCompleted :smsmessage
04-02 17:37:50.394: INFO/LMSMSReader(7651): onDestroy

Plumbing

That’s cool, but how does Android know to call our application when a message arrives? The answer lies in the AndroidManifest.xml file.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.navitend.lm.smsreader"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name" >
		<receiver android:name=".LMSMSReader">
			<intent-filter>
				<action android:name="android.provider.Telephony.SMS_RECEIVED"></action>
			</intent-filter>
		</receiver>
	    <activity android:name=".ReadThisMessage" />
    </application>
	<uses-permission android:name="android.permission.RECEIVE_SMS"></uses-permission>
</manifest>

The “receiver” tag calls out the BroadcastReceiver and the IntentFilter specifies which event we are interested in.

The permission: “android.permission.RECEIVE_SMS” is also required in order to process the incoming messages.

Lastly, don’t forget to define the Activity “ReadThisMessage”, otherwise it won’t start when asked to read the message!. Note that unlike many Android applications the sole Activity in this application does NOT show up on the home screen and in fact doesn’t even have a UI.

Of course, there is always more to do. I often get automated text messages related to voice mail notifications, backup notifications or after-hours service emergencies, etc. I certainly don’t want everyone of those messages read aloud. Nor do I necessarily want them read while I am sleeping. Or perhaps I do to help me wake up? Regardless of the specifics, it looks like there is more work to do here. Allowing the user to customize the behavior of the application via Android Preferences will be necessary and something we look at in a future article.

Fatal error: Call to undefined function aa_author_bios() in /opt/apache/dms/b2b/linux-mag.com/site/www/htdocs/wp-content/themes/linuxmag/single.php on line 62