Telephony and SMS - Professional Android 4 Application Development (2012)

Professional Android 4 Application Development (2012)

Chapter 17. Telephony and SMS

What's in this Chapter?

Initiating phone calls

Reading the phone, network, data connectivity, and SIM states

Monitoring changes to the phone, network, data connectivity, and SIM states

Using Intents to send SMS and MMS messages

Using the SMS Manager to send SMS messages

Handling incoming SMS messages

In this chapter, you'll learn to use Android's telephony APIs to monitor mobile voice and data connections as well as incoming and outgoing calls, and to send and receive short messaging service (SMS) messages.

You'll take a look at the communication hardware by examining the telephony package for monitoring phone state and phone calls, as well as initiating calls and monitoring incoming call details.

Android also offers full access to SMS functionality, letting you send and receive SMS messages from within your applications. Using the Android APIs, you can create your own SMS client application to replace the native clients available as part of the software stack. Alternatively, you can incorporate the messaging functionality within your own applications.

Hardware Support for Telephony

With the arrival of Wi-Fi-only Android devices, you can no longer assume that telephony will be supported on all the hardware on which your application may be available.

Marking Telephony as a Required Hardware Feature

Some applications don't make sense on devices that don't have telephony support. An application that provides reverse-number lookup for incoming calls or a replacement SMS client simply won't work on a Wi-Fi-only device.

To specify that your application requires telephony support to function, you can add a uses-feature node to your application manifest:

<uses-feature android:name="android.hardware.telephony"
              android:required="true"/>

2.1

Marking telephony as a required feature prevents your application from being found on Google Play using a device without telephony hardware. It also prevents your application from being installed on such devices from the Google Play website.

Checking for Telephony Hardware

If you use telephony APIs but they aren't strictly necessary for your application to be used, you can check for the existence of telephony hardware before attempting to make use of the related APIs.

Use the Package Manager's hasSystemFeature method, specifying the FEATURE_TELEPHONY feature. The Package Manager also includes constants to query the existence of CDMA- and GSM-specific hardware.

PackageManager pm = getPackageManager();
 
boolean telephonySupported =
  pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
boolean gsmSupported = 
  pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CDMA);
boolean cdmaSupported = 
  pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_GSM);

It's good practice to check for telephony support early in your application's lifecycle and adjust its UI and behavior accordingly.

Using Telephony

The Android telephony APIs let your applications access the underlying telephone hardware stack, making it possible to create your own dialer—or integrate call handling and phone state monitoring into your applications.

2.1

Because of security concerns, the current Android SDK does not allow you to create your own in-call Activity—the screen that is displayed when an incoming call is received or an outgoing call has been placed.

The following sections focus on how to monitor and control phone, service, and cell events in your applications to augment and manage the native phone-handling functionality. You can use the same techniques to implement a replacement dialer application.

Initiating Phone Calls

Best practice for initiating phone calls is to use an Intent.ACTION_DIAL Intent, specifying the number to dial by setting the Intents data using a tel: schema:

Intent whoyougonnacall = new Intent(Intent.ACTION_DIAL, 
                                    Uri.parse("tel:555-2368"));
startActivity(whoyougonnacall);

This starts a dialer Activity that should be prepopulated with the number you specified. The default dialer Activity allows the user to change the number before explicitly initiating the call. As a result, using the ACTION_DIAL Intent action doesn't require any special permissions.

By using an Intent to announce your intention to dial a number, your application stays decoupled from the dialer implementation used to initiate the call. For example, if users have installed a new dialer that supports IP-based telephony, using Intents to dial a number from your application lets them use this new dialer.

Replacing the Native Dialer

Replacing the native dialer application involves two steps:

1. Intercept Intents serviced by the native dialer.

2. Initiate and manage outgoing calls.

The native dialer application responds to Intent actions corresponding to a user pressing the hardware call button, asking to view data using the tel: schema, or making an ACTION_DIAL request using the tel: schema, as shown in the previous section.

To intercept these requests, include intent-filter tags on the manifest entries for your replacement dialer Activity that listens for the following actions:

· Intent.ACTION_CALL_BUTTON—This action is broadcast when the device's hardware call button is pressed. Create an Intent Filter that listens for this action as a default action.

· Intent.ACTION_DIAL—This Intent action, described in the previous section, is used by applications that want to initiate a phone call. The Intent Filter used to capture this action should be both default and browsable (to support dial requests from the browser) and must specify the tel: schema to replace existing dialer functionality (though it can support additional schemes).

· Intent.ACTION_VIEW—The view action is used by applications wanting to view a piece of data. Ensure that the Intent Filter specifies the tel: schema to allow your new Activity to be used to view telephone numbers.

The manifest snippet in Listing 17.1 shows an Activity with Intent Filters that will capture each of these actions.

2.11

Listing 17.1: Manifest entry for a replacement dialer Activity

<activity
  android:name=".MyDialerActivity"
  android:label="@string/app_name">
  <intent-filter>
    <action android:name="android.intent.action.CALL_BUTTON" />
    <category android:name="android.intent.category.DEFAULT" />
  </intent-filter>
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <action android:name="android.intent.action.DIAL" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="tel" />
  </intent-filter>
</activity>

code snippet PA3AD_Ch17_Replacement_Dialer/AndroidManifest.xml

After your Activity has been started, it should provide a UI that allows users to enter or modify the number to dial and to initiate the outgoing call. At that point you need to place the call—using either the existing telephony stack or your own alternative.

The simplest technique is to use the existing telephony stack using the Intent.ACTION_CALL action, as shown in Listing 17.2.

Listing 17.2: Initiating a call using the system telephony stack

Intent whoyougonnacall = new Intent(Intent.ACTION_CALL, 
                                    Uri.parse("tel:555-2368"));
startActivity(whoyougonnacall);

code snippet PA3AD_Ch17_Replacement_Dialer/AndroidManifest.xml

This will initiate a call using the system in-call Activity and will let the system manage the dialing, connection, and voice handling.

To use this action, your application must request the CALL_PHONE uses-permission:

<uses-permission android:name="android.permission.CALL_PHONE"/>

Alternatively, you can completely replace the outgoing telephony stack by implementing your own dialing and voice-handling framework. This is the perfect alternative if you are implementing a VOIP (voice over IP) application.

Note, also, that you can use the preceding techniques to intercept outgoing call Intents and modify outgoing numbers or to block outgoing calls as an alternative to completely replacing the dialer.

Accessing Telephony Properties and Phone State

Access to the telephony APIs is managed by the Telephony Manager, accessible using the getSystemService method:

String srvcName = Context.TELEPHONY_SERVICE;
TelephonyManager telephonyManager = 
  (TelephonyManager)getSystemService(srvcName);

The Telephony Manager provides direct access to many of the phone properties, including device, network, subscriber identity module (SIM), and data state details. You can also access some connectivity status information, although this is usually done using the Connectivity Manager, as described in the previous chapter.

Reading Phone Device Details

Using the Telephony Manager, you can obtain the phone type (GSM CDMA, or SIP), unique ID (IMEI or MEID), software version, and the phone's phone number:

String phoneTypeStr = "unknown";
 
int phoneType = telephonyManager.getPhoneType();
switch (phoneType) {
  case (TelephonyManager.PHONE_TYPE_CDMA): 
    phoneTypeStr = "CDMA"; 
    break;
  case (TelephonyManager.PHONE_TYPE_GSM) : 
    phoneTypeStr = "GSM"; 
    break;
  case (TelephonyManager.PHONE_TYPE_SIP): 
    phoneTypeStr = "SIP"; 
    break;
  case (TelephonyManager.PHONE_TYPE_NONE): 
    phoneTypeStr = "None"; 
    break;
  default: break;
}
 
// -- These require READ_PHONE_STATE uses-permission --
// Read the IMEI for GSM or MEID for CDMA
String deviceId = telephonyManager.getDeviceId();
// Read the software version on the phone (note -- not the SDK version)
String softwareVersion = telephonyManager.getDeviceSoftwareVersion();
// Get the phone's number (if available)
String phoneNumber = telephonyManager.getLine1Number();

Note that, except for the phone type, reading each of these properties requires that the READ_PHONE_STATE uses-permission be included in the application manifest:

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

You can also determine the type of network you're connected to, along with the name and country of the SIM or connected carrier network.

Reading Network Details

When your device is connected to a network, you can use the Telephony Manager to read the Mobile Country Code and Mobile Network Code (MCC+MNC), the country ISO code, the network operator name, and the type of network you're connected to using the getNetworkOperator,getNetworkCountryIso, getNetworkOperatorName, and getNetworkType methods:

// Get connected network country ISO code
String networkCountry = telephonyManager.getNetworkCountryIso();
// Get the connected network operator ID (MCC + MNC)
String networkOperatorId = telephonyManager.getNetworkOperator();
// Get the connected network operator name
String networkName = telephonyManager.getNetworkOperatorName();
 
// Get the type of network you are connected to
int networkType = telephonyManager.getNetworkType();
switch (networkType) {
  case (TelephonyManager.NETWORK_TYPE_1xRTT)   : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_CDMA)    : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_EDGE)    : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_EHRPD)   : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_EVDO_0)  : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_EVDO_A)  : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_EVDO_B)  : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_GPRS)    : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_HSDPA)   : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_HSPA)    : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_HSPAP)   : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_HSUPA)   : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_IDEN)    : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_LTE)     : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_UMTS)    : [… do something …]
                                                 break;
  case (TelephonyManager.NETWORK_TYPE_UNKNOWN) : [… do something …]
                                                 break;
  default: break;
}

These commands work only when you are connected to a mobile network and can be unreliable if it is a CDMA network. Use the getPhoneType method, as shown in the preceding code snippet, to determine which phone type is being used.

Reading SIM Details

If your application is running on a GSM device, it will usually have a SIM. You can query the SIM details from the Telephony Manager to obtain the ISO country code, operator name, and operator MCC and MNC for the SIM installed in the current device. These details can be useful if you need to provide specialized functionality for a particular carrier.

If you have included the READ_PHONE_STATE uses-permission in your application manifest, you can also obtain the serial number for the current SIM using the getSimSerialNumber method when the SIM is in a ready state.

Before you can use any of these methods, you must ensure that the SIM is in a ready state. You can determine this using the getSimState method:

int simState = telephonyManager.getSimState();
switch (simState) {
  case (TelephonyManager.SIM_STATE_ABSENT): break;
  case (TelephonyManager.SIM_STATE_NETWORK_LOCKED): break;
  case (TelephonyManager.SIM_STATE_PIN_REQUIRED): break;
  case (TelephonyManager.SIM_STATE_PUK_REQUIRED): break;
  case (TelephonyManager.SIM_STATE_UNKNOWN): break;
  case (TelephonyManager.SIM_STATE_READY): {
    // Get the SIM country ISO code
    String simCountry = telephonyManager.getSimCountryIso();
    // Get the operator code of the active SIM (MCC + MNC)
    String simOperatorCode = telephonyManager.getSimOperator();
    // Get the name of the SIM operator
    String simOperatorName = telephonyManager.getSimOperatorName();
    // -- Requires READ_PHONE_STATE uses-permission --
    // Get the SIM's serial number
    String simSerial = telephonyManager.getSimSerialNumber();
    break;
  }
  default: break;
}

Reading Data Connection and Transfer State Details

Using the getDataState and getDataActivity methods, you can find the current data connection state and data transfer activity, respectively:

int dataActivity = telephonyManager.getDataActivity();
int dataState = telephonyManager.getDataState();
 
switch (dataActivity) {
  case TelephonyManager.DATA_ACTIVITY_IN : break;
  case TelephonyManager.DATA_ACTIVITY_OUT : break;
  case TelephonyManager.DATA_ACTIVITY_INOUT : break;
  case TelephonyManager.DATA_ACTIVITY_NONE : break;
}
 
switch (dataState) {
  case TelephonyManager.DATA_CONNECTED : break;
  case TelephonyManager.DATA_CONNECTING : break;
  case TelephonyManager.DATA_DISCONNECTED : break;
  case TelephonyManager.DATA_SUSPENDED : break;
}

2.1

The Telephony Manager indicates only telephony-based data connectivity (mobile data as opposed to Wi-Fi). As a result, in most circumstances the Connectivity Manager is a better alternative to determine the current connectivity state.

Monitoring Changes in Phone State Using the Phone State Listener

The Android telephony APIs lets you monitor changes to phone state and associated details such as incoming phone numbers.

Changes to the phone state are monitored using the PhoneStateListener class, with some state changes also broadcast as Intents. This section describes how to use the Phone State Listener, and the following section describes which Broadcast Intents are available.

To monitor and manage phone state, your application must specify the READ_PHONE_STATE uses-permission:

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

Create a new class that implements the Phone State Listener to monitor, and respond to, phone state change events, including call state (ringing, off hook, and so on), cell location changes, voice-mail and call-forwarding status, phone service changes, and changes in mobile signal strength.

Within your Phone State Listener implementation, override the event handlers of the events you want to react to. Each handler receives parameters that indicate the new phone state, such as the current cell location, call state, or signal strength.

After creating your own Phone State Listener, register it with the Telephony Manager using a bitmask to indicate the events you want to listen for:

telephonyManager.listen(phoneStateListener,
                        PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR|
                        PhoneStateListener.LISTEN_CALL_STATE |
                        PhoneStateListener.LISTEN_CELL_LOCATION |
                        PhoneStateListener.LISTEN_DATA_ACTIVITY |
                        PhoneStateListener.LISTEN_DATA_CONNECTION_STATE |
                        PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR |
                        PhoneStateListener.LISTEN_SERVICE_STATE |
                        PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);

To unregister a listener, call listen and pass in PhoneStateListener.LISTEN_NONE as the bitmask parameter:

telephonyManager.listen(phoneStateListener,
                        PhoneStateListener.LISTEN_NONE);

2.1

Your Phone State Listener will receive phone state change notifications only while your application is running.

Monitoring Incoming Phone Calls

If your application should respond to incoming phone calls only while it is running, you can override the onCallStateChanged method in your Phone State Listener implementation, and register it to receive notifications when the call state changes:

PhoneStateListener callStateListener = new PhoneStateListener() {
  public void onCallStateChanged(int state, String incomingNumber) {
    String callStateStr = "Unknown";
    
    switch (state) {
      case TelephonyManager.CALL_STATE_IDLE : 
        callStateStr = "idle"; break;
      case TelephonyManager.CALL_STATE_OFFHOOK : 
        callStateStr = "offhook"; break;
      case TelephonyManager.CALL_STATE_RINGING : 
        callStateStr = "ringing. Incoming number is: " 
        + incomingNumber; 
        break;
      default : break;
    }
    
    Toast.makeText(MyActivity.this, 
      callStateStr, Toast.LENGTH_LONG).show();
  }
};
 
telephonyManager.listen(callStateListener,
                        PhoneStateListener.LISTEN_CALL_STATE);

The onCallStateChanged handler receives the phone number associated with incoming calls, and the state parameter represents the current call state as one of the following three values:

· TelephonyManager.CALL_STATE_IDLE—When the phone is neither ringing nor in a call

· TelephonyManager.CALL_STATE_RINGING—When the phone is ringing

· TelephonyManager.CALL_STATE_OFFHOOK—When the phone is currently in a call

Note that as soon as the state changes to CALL_STATE_RINGING, the system will display the incoming call screen, asking users if they want to answer the call.

Your application must be running to receive this callback. If your application should be started whenever the phone state changes, you can register an Intent Receiver that listens for a Broadcast Intent signifying a change in phone state. This is described in the “Using Intent Receivers to Monitor Incoming Phone Calls” section later in this chapter.

Tracking Cell Location Changes

You can get notifications whenever the current cell location changes by overriding onCellLocationChanged on a Phone State Listener implementation. Before you can register to listen for cell location changes, you need to add the ACCESS_COARSE_LOCATION permission to your application manifest:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

The onCellLocationChanged handler receives a CellLocation object that includes methods for extracting different location information based on the type of phone network. In the case of a GSM network, the cell ID (getCid) and the current location area code (getLac) are available. For CDMA networks, you can obtain the current base station ID (getBaseStationId) and the latitude (getBaseStationLatitude) and longitude (getBaseStationLongitude) of that base station.

The following code snippet shows how to implement a Phone State Listener to monitor cell location changes, displaying a Toast that includes the received network location details.

PhoneStateListener cellLocationListener = new PhoneStateListener() {
  public void onCellLocationChanged(CellLocation location) {
    if (location instanceof GsmCellLocation) {
      GsmCellLocation gsmLocation = (GsmCellLocation)location;
      Toast.makeText(getApplicationContext(),
                     String.valueOf(gsmLocation.getCid()),
                    Toast.LENGTH_LONG).show();
    }
    else if (location instanceof CdmaCellLocation) {
      CdmaCellLocation cdmaLocation = (CdmaCellLocation)location;
      StringBuilder sb = new StringBuilder();
      sb.append(cdmaLocation.getBaseStationId());
      sb.append("\n@");
      sb.append(cdmaLocation.getBaseStationLatitude());
      sb.append(cdmaLocation.getBaseStationLongitude());
    
      Toast.makeText(getApplicationContext(),
                     sb.toString(),
                     Toast.LENGTH_LONG).show();
    }
  }
};
telephonyManager.listen(cellLocationListener,
                        PhoneStateListener.LISTEN_CELL_LOCATION);

Tracking Service Changes

The onServiceStateChanged handler tracks the service details for the device's cell service. Use the ServiceState parameter to find details of the current service state.

The getState method on the Service State object returns the current service state as one of the following ServiceState constants:

· STATE_IN_SERVICE—Normal phone service is available.

· STATE_EMERGENCY_ONLY—Phone service is available but only for emergency calls.

· STATE_OUT_OF_SERVICE—No cell phone service is currently available.

· STATE_POWER_OFF—The phone radio is turned off (usually when airplane mode is enabled).

A series of getOperator* methods is available to retrieve details on the operator supplying the cell phone service, whereas getRoaming tells you if the device is currently using a roaming profile:

PhoneStateListener serviceStateListener = new PhoneStateListener() {
  public void onServiceStateChanged(ServiceState serviceState) {
    if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) {
      String toastText = "Operator: " + serviceState.getOperatorAlphaLong();
      Toast.makeText(MyActivity.this, toastText, Toast.LENGTH_SHORT);
    }
  }
};
 
telephonyManager.listen(serviceStateListener,
                        PhoneStateListener.LISTEN_SERVICE_STATE);

Monitoring Data Connectivity and Data Transfer Status Changes

You can use a Phone State Listener to monitor changes in mobile data connectivity and mobile data transfer. Note that this does not include data transferred using Wi-Fi. For more comprehensive monitoring of data connectivity and transfers, use the Connectivity Manager, as described in the previous chapter.

The Phone State Listener includes two event handlers for monitoring the device's data connection. Override onDataActivity to track data transfer activity, and onDataConnectionStateChanged to request notifications for data connection state changes:

PhoneStateListener dataStateListener = new PhoneStateListener() {
  public void onDataActivity(int direction) {
    String dataActivityStr = "None";
    
    switch (direction) {
      case TelephonyManager.DATA_ACTIVITY_IN : 
        dataActivityStr = "Downloading"; break;
      case TelephonyManager.DATA_ACTIVITY_OUT : 
        dataActivityStr = "Uploading"; break;
      case TelephonyManager.DATA_ACTIVITY_INOUT : 
        dataActivityStr = "Uploading/Downloading"; break;
      case TelephonyManager.DATA_ACTIVITY_NONE : 
        dataActivityStr = "No Activity"; break;
    }
    
    Toast.makeText(MyActivity.this, 
      "Data Activity is " + dataActivityStr, 
      Toast.LENGTH_LONG).show();
  }
 
  public void onDataConnectionStateChanged(int state) {
    String dataStateStr = "Unknown";
 
    switch (state) {
      case TelephonyManager.DATA_CONNECTED : 
        dataStateStr = "Connected"; break;
      case TelephonyManager.DATA_CONNECTING : 
        dataStateStr = "Connecting"; break;
      case TelephonyManager.DATA_DISCONNECTED : 
        dataStateStr = "Disconnected"; break;
      case TelephonyManager.DATA_SUSPENDED : 
        dataStateStr = "Suspended"; break;
    }
    
    Toast.makeText(MyActivity.this, 
      "Data Connectivity is " + dataStateStr, 
      Toast.LENGTH_LONG).show();
  }
};
 
telephonyManager.listen(dataStateListener,
                        PhoneStateListener.LISTEN_DATA_ACTIVITY |
                        PhoneStateListener.LISTEN_DATA_CONNECTION_STATE); 

Using Intent Receivers to Monitor Incoming Phone Calls

When the phone state changes as a result of an incoming, accepted, or terminated phone call, the Telephony Manager will broadcast an ACTION_PHONE_STATE_CHANGED Intent.

By registering a manifest Intent Receiver that listens for this Broadcast Intent, as shown in the snippet below, you can listen for incoming phone calls at any time, even if your application isn't running. Note that your application needs to request the READ_PHONE_STATE permission to receive the phone state changed Broadcast Intent.

<receiver android:name="PhoneStateChangedReceiver">
  <intent-filter>
    <action android:name="android.intent.action.PHONE_STATE"></action>
  </intent-filter>    
</receiver>

The Phone State Changed Broadcast Intent includes up to two extras. All such broadcasts will include the EXTRA_STATE extra, whose value will be one of the TelephonyManager.CALL_STATE_* actions described earlier to indicate the new phone state. If the state is ringing, the Broadcast Intent will also include the EXTRA_INCOMING_NUMBER extra, whose value represents the incoming call number.

The following skeleton code can be used to extract the current phone state and incoming call number where it exists:

public class PhoneStateChangedReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    String phoneState = intent.getStringExtra(TelephonyManager.EXTRA_STATE); 
    if (phoneState.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
      String phoneNumber =
        intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
      Toast.makeText(context, 
        "Incoming Call From: " + phoneNumber, 
        Toast.LENGTH_LONG).show();
    }
  }
}

Introducing SMS and MMS

If you own a mobile phone that's less than two decades old, chances are you're familiar with SMS messaging. SMS is now one of the most-used communication mechanisms on mobile phones.

SMS technology is designed to send short text messages between mobile phones. It provides support for sending both text messages (designed to be read by people) and data messages (meant to be consumed by applications). Multimedia messaging service (MMS) messages allow users to send and receive messages that include multimedia attachments such as photos, videos, and audio.

Because SMS and MMS are mature mobile technologies, there's a lot of information out there that describes the technical details of how an SMS or MMS message is constructed and transmitted over the air. Rather than rehash that information here, the following sections focus on the practicalities of sending and receiving text, data, and multimedia messages from within Android applications.

Using SMS and MMS in Your Application

Android provides support for sending both SMS and MMS messages using a messaging application installed on the device with the SEND and SEND_TO Broadcast Intents.

Android also supports full SMS functionality within your applications through the SmsManager class. Using the SMS Manager, you can replace the native SMS application to send text messages, react to incoming texts, or use SMS as a data transport layer.

At this time, the Android API does not include simple support for creating MMS messages from within your applications.

This chapter demonstrates how to use both the SMS Manager and Intents to send messages from within your applications.

SMS message delivery is not timely. Compared to using an IP- or socket-based transport, using SMS to pass data messages between applications is slow, possibly expensive, and can suffer from high latency. As a result, SMS is not suitable for anything that requires real-time responsiveness. That said, the widespread adoption and resiliency of SMS networks make it a particularly good tool for delivering content to non-Android users and reducing the dependency on third-party servers.

Sending SMS and MMS from Your Application Using Intents

In most cases it's best practice to use an Intent to send SMS and MMS messages using another application—typically the native SMS application—rather than implementing a full SMS client.

To do so, call startActivity with an Intent.ACTION_SENDTO action Intent. Specify a target number using sms: schema notation as the Intent data. Include the message you want to send within the Intent payload using an sms_body extra:

Intent smsIntent = new Intent(Intent.ACTION_SENDTO,
                              Uri.parse("sms:55512345"));
smsIntent.putExtra("sms_body", "Press send to send me");
startActivity(smsIntent);

To attach files to your message (effectively creating an MMS message), add an Intent.EXTRA_STREAM with the URI of the resource to attach, and set the Intent type to the MIME type of the attached resource.

Note that the native MMS application doesn't include an Intent Receiver for ACTION_SENDTO with a type set. Instead, you need to use ACTION_SEND and include the target phone number as an address extra:

// Get the URI of a piece of media to attach.
Uri attached_Uri 
  = Uri.parse("content://media/external/images/media/1");
 
// Create a new MMS intent
Intent mmsIntent = new Intent(Intent.ACTION_SEND, attached_Uri);
mmsIntent.putExtra("sms_body", "Please see the attached image");
mmsIntent.putExtra("address", "07912355432");
mmsIntent.putExtra(Intent.EXTRA_STREAM, attached_Uri);
mmsIntent.setType("image/jpeg");
startActivity(mmsIntent);

2.1

When running the MMS example shown in Listing 17.2, users are likely to be prompted to select one of a number of applications capable of fulfilling the send request, including the Gmail, email, and SMS applications.

Sending SMS Messages Using the SMS Manager

SMS messaging in Android is handled by the SmsManager class. You can get a reference to the SMS Manager using the static SmsManager.getDefault method:

SmsManager smsManager = SmsManager.getDefault();

2.1

Prior to Android 1.6 (SDK level 4), the SmsManager and SmsMessage classes were provided by the android.telephony.gsm package. These have since been deprecated and the SMS classes moved to android.telephony to ensure generic support for GSM and CDMA devices.

To send SMS messages, your application must specify the SEND_SMS uses-permission:

<uses-permission android:name="android.permission.SEND_SMS"/>

Sending Text Messages

To send a text message, use sendTextMessage from the SMS Manager, passing in the address (phone number) of your recipient and the text message you want to send:

SmsManager smsManager = SmsManager.getDefault();
 
String sendTo = "5551234";
String myMessage = "Android supports programmatic SMS messaging!";
 
smsManager.sendTextMessage(sendTo, null, myMessage, null, null);

The second parameter can be used to specify the SMS service center to use. If you enter null, the default service center for the device's carrier will be used.

The final two parameters let you specify Intents to track the transmission and successful delivery of your messages. To react to these Intents, create and register Broadcast Receivers, as shown in the section “Tracking and Confirming SMS Message Delivery.”

2.1

The Android debugging bridge supports sending SMS messages among multiple emulator instances. To send an SMS from one emulator to another, specify the port number of the target emulator as the “to” address when sending a new message. Android will route your message to the target emulator instance, where it will be received as a normal SMS.

Tracking and Confirming SMS Message Delivery

To track the transmission and delivery success of your outgoing SMS messages, implement and register Broadcast Receivers that listen for the actions you specify when creating the Pending Intents you pass in to the sendTextMessage method.

The first Pending Intent parameter is fired when the message is either successfully sent or fails to send. The result code for the Broadcast Receiver that receives this Intent will be one of the following:

· Activity.RESULT_OK—To indicate a successful transmission

· SmsManager.RESULT_ERROR_GENERIC_FAILURE—To indicate a nonspecific failure

· SmsManager.RESULT_ERROR_RADIO_OFF—To indicate the phone radio is turned off

· SmsManager.RESULT_ERROR_NULL_PDU—To indicate a PDU (protocol description unit) failure

· SmsManager.RESULT_ERROR_NO_SERVICE—To indicate that no cellular service is currently available

The second Pending Intent parameter is fired only after the recipient receives your SMS message.

The following code snippet shows the typical pattern for sending an SMS and monitoring the success of its transmission and delivery.

String SENT_SMS_ACTION = "com.paad.smssnippets.SENT_SMS_ACTION";
String DELIVERED_SMS_ACTION = "com.paad.smssnippets.DELIVERED_SMS_ACTION";
 
// Create the sentIntent parameter
Intent sentIntent = new Intent(SENT_SMS_ACTION);
PendingIntent sentPI = PendingIntent.getBroadcast(getApplicationContext(),
                                                  0,
                                                  sentIntent,
                                                  PendingIntent.FLAG_UPDATE_CURRENT);
 
// Create the deliveryIntent parameter
Intent deliveryIntent = new Intent(DELIVERED_SMS_ACTION);
PendingIntent deliverPI = 
  PendingIntent.getBroadcast(getApplicationContext(),
                             0,
                             deliveryIntent,
                             PendingIntent.FLAG_UPDATE_CURRENT);
 
// Register the Broadcast Receivers
registerReceiver(new BroadcastReceiver() {
                   @Override
                   public void onReceive(Context _context, Intent _intent)
                   {
                     String resultText = "UNKNOWN";
                     
                     switch (getResultCode()) {
                       case Activity.RESULT_OK:
                         resultText = "Transmission successful"; break;
                       case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
                         resultText = "Transmission failed"; break;
                       case SmsManager.RESULT_ERROR_RADIO_OFF:
                         resultText = "Transmission failed: Radio is off";
                         break;
                       case SmsManager.RESULT_ERROR_NULL_PDU:
                         resultText = "Transmission Failed: No PDU specified";
                         break;
                       case SmsManager.RESULT_ERROR_NO_SERVICE:
                         resultText = "Transmission Failed: No service";
                         break;
                     }
                     Toast.makeText(_context, resultText,
                                    Toast.LENGTH_LONG).show();
                   }
                 },
                 new IntentFilter(SENT_SMS_ACTION));
 
registerReceiver(new BroadcastReceiver() {
                   @Override
                   public void onReceive(Context _context, Intent _intent)
                   {
                     Toast.makeText(_context, "SMS Delivered",
                                    Toast.LENGTH_LONG).show();
                   }
                 },
                 new IntentFilter(DELIVERED_SMS_ACTION));
 
// Send the message
SmsManager smsManager = SmsManager.getDefault();
String sendTo = "5551234";
String myMessage = "Android supports programmatic SMS messaging!";
 
smsManager.sendTextMessage(sendTo, null, myMessage, sentPI, deliverPI); 

Conforming to the Maximum SMS Message Size

The maximum length of each SMS text message can vary by carrier, but are typically limited to 160 characters. As a result longer messages need to be broken into a series of smaller parts. The SMS Manager includes the divideMessage method, which accepts a string as an input and breaks it into an Array List of messages, wherein each is less than the maximum allowable size.

You can then use the sendMultipartTextMessage method on the SMS Manager to transmit the array of messages:

ArrayList<String> messageArray = smsManager.divideMessage(myMessage);
ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>();
for (int i = 0; i < messageArray.size(); i++)
  sentIntents.add(sentPI);
 
smsManager.sendMultipartTextMessage(sendTo,
                                    null,
                                    messageArray,
                                    sentIntents, null);

The sentIntent and deliveryIntent parameters in the sendMultipartTextMessage method are Array Lists that can be used to specify different Pending Intents to fire for each message part.

Sending Data Messages

You can send binary data via SMS using the sendDataMessage method on an SMS Manager. The sendDataMessage method works much like sendTextMessage but includes additional parameters for the destination port and an array of bytes that constitutes the data you want to send.

String sendTo = "5551234";
short destinationPort = 80;
byte[] data = [ … your data … ];
 
smsManager.sendDataMessage(sendTo, null, destinationPort,
                           data, null, null);

Listening for Incoming SMS Messages

When a device receives a new SMS message, a new Broadcast Intent is fired with the android.provider.Telephony.SMS_RECEIVED action. Note that this is a string literal; the SDK currently doesn't include a reference to this string, so you must specify it explicitly when using it in your applications.

2.1

The SMS received action string is hidden—and therefore an unsupported API. As such, it is subject to change with any future platform release. It is not good practice to use unsupported APIs because doing so carries significant risk. Be cautious when using unsupported platform features because they are subject to change in future platform releases.

For an application to listen for SMS Broadcast Intents, it needs to specify the RECEIVE_SMS manifest permission:

<uses-permission
 android:name="android.permission.RECEIVE_SMS"
/>

The SMS Broadcast Intent includes the incoming SMS details. To extract the array of SmsMessage objects packaged within the SMS Broadcast Intent bundle, use the pdu key to extract an array of SMS PDUs (protocol data units—used to encapsulate an SMS message and its metadata), each of which represents an SMS message, from the extras Bundle. To convert each PDU byte array into an SMS Message object, call SmsMessage.createFromPdu, passing in each byte array:

Bundle bundle = intent.getExtras();
if (bundle != null) {
  Object[] pdus = (Object[]) bundle.get("pdus");
  SmsMessage[] messages = new SmsMessage[pdus.length];
  for (int i = 0; i < pdus.length; i++)
    messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
}

Each SmsMessage contains the SMS message details, including the originating address (phone number), timestamp, and the message body, which can be extracted using the getOriginatingAddress, getTimestampMillis, and getMessageBody methods, respectively:

public class MySMSReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    Bundle bundle = intent.getExtras();
    if (bundle != null) {
      Object[] pdus = (Object[]) bundle.get("pdus");
      SmsMessage[] messages = new SmsMessage[pdus.length];
      for (int i = 0; i < pdus.length; i++)
        messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
      
      for (SmsMessage message : messages) {
        String msg = message.getMessageBody();
        long when = message.getTimestampMillis();
        String from = message.getOriginatingAddress();
 
        Toast.makeText(context, from + " : " + msg,
                       Toast.LENGTH_LONG).show();
      }
    }
  }
}

To listen for incoming messages, register your SMS Broadcast Receiver using an Intent Filter that listens for the android.provider.Telephony.SMS_RECEIVED action String. In most circumstances you'll want to register this in the application manifest to ensure your application can always respond to incoming SMS messages.

<receiver android:name="MySMSReceiver">
  <intent-filter>
    <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
  </intent-filter>    
</receiver> 

Simulating Incoming SMS Messages in the Emulator

Two techniques are available for simulating incoming SMS messages in the emulator. The first was described previously in this section: you can send an SMS message from one emulator to another by using its port number as its phone number.

Alternatively, you can use the Android debug tools (introduced in Chapter 2, “Getting Started”) to simulate incoming SMS messages from arbitrary numbers, as shown in Figure 17.1.

Figure 17.1

17.1

Handling Data SMS Messages

Data messages are received in the same way as normal SMS text messages, and are extracted in the same way as shown in the preceding section. To extract the data transmitted within a data SMS, use the getUserData method:

byte[] data = msg.getUserData();

The getUserData method returns a byte array of the data included in the message.

Emergency Responder SMS Example

In this example, you'll create an SMS application that turns an Android phone into an emergency response beacon.

When finished, the next time you're in an unfortunate proximity to an alien invasion or find yourself in a robot-uprising scenario, you can set your phone to automatically respond to your friends' and family members' pleas for a status update with a friendly message (or a desperate cry for help).

To make things easier for your would-be saviors, you can use location-based services to tell your rescuers exactly where to find you. The robustness of SMS network infrastructure makes SMS an excellent option for applications like this, for which reliability is critical.

1. Start by creating a new EmergencyResponder project that features an EmergencyResponder Activity:

package com.paad.emergencyresponder;
 
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.locks.ReentrantLock;
 
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.Address;
import android.location.Geocoder;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ListView;
 
public class EmergencyResponder extends Activity {
  
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
  }
  
}

2. Add permissions for finding your location, and for sending and receiving incoming SMS messages to the manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.paad.emergencyresponder"
  android:versionCode="1"
  android:versionName="1.0" >
 
  <uses-permission android:name="android.permission.RECEIVE_SMS"/>
  <uses-permission android:name="android.permission.SEND_SMS"/>
  <uses-permission
    android:name="android.permission.ACCESS_FINE_LOCATION"/>
 
  <uses-sdk android:targetSdkVersion="15"/>
 
  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name" >
    <activity
      android:name=".EmergencyResponder"
      android:label="@string/app_name" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>
 
</manifest>

3. Update the res/values/strings.xml resource to include the text to display within the “all clear” and “mayday” buttons, as well as their associated default response messages. You should also define an incoming message text that the application will use to detect requests for a status response:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="app_name">Emergency Responder</string>
  <string name="allClearButtonText">I am Safe and Well
  </string>
  <string name="maydayButtonText">MAYDAY! MAYDAY! MAYDAY!
  </string>
  <string name="setupautoresponderButtonText">Setup Auto Responder</string>
  <string name="allClearText">I am safe and well. Worry not!
  </string>
  <string name="maydayText">Tell my mother I love her.
  </string>
  <string name="querystring">are you OK?</string>
  <string name="querylistprompt">These people want to know if you\'re ok</string>
  <string name="includelocationprompt">Include Location in Reply</string>
</resources>

4. Modify the main.xml layout resource. Include a ListView to display the list of people requesting a status update and a series of buttons that will allow the user to send response SMS messages:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <TextView
    android:id="@+id/labelRequestList"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/querylistprompt"
    android:layout_alignParentTop="true"
  />
  <LinearLayout
    android:id="@+id/buttonLayout"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="5dp"
    android:layout_alignParentBottom="true">
    <CheckBox
      android:id="@+id/checkboxSendLocation"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/includelocationprompt"/>
    <Button
      android:id="@+id/okButton"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/allClearButtonText"/>
    <Button
      android:id="@+id/notOkButton"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/maydayButtonText"/>
    <Button
      android:id="@+id/autoResponder"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/setupautoresponderButtonText"/>
  </LinearLayout>
  <ListView
    android:id="@+id/myListView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_below="@id/labelRequestList"
    android:layout_above="@id/buttonLayout"/>
</RelativeLayout>

At this point, the GUI will be complete, so starting the application should show you the screen in Figure 17.2.

5. Now create a new Array List of Strings within the EmergencyResponder Activity to store the phone numbers of the incoming requests for your status. Bind the Array List to the List View using an Array Adapter in the Activity's onCreate method, and create a new ReentrantLock object to ensure thread-safe handling of the Array List. Take this opportunity to get a reference to the check box and to add Click Listeners for each response button. Each button should call the respond method, whereas the Setup Auto Responder button should call the startAutoResponder stub.

Figure 17.2

17.2

ReentrantLock lock;
CheckBox locationCheckBox;
ArrayList<String> requesters;
ArrayAdapter<String> aa;
 
@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
 
  lock = new ReentrantLock();
  requesters = new ArrayList<String>();
  wireUpControls();
}
 
private void wireUpControls() {
  locationCheckBox = (CheckBox)findViewById(R.id.checkboxSendLocation);
  ListView myListView = (ListView)findViewById(R.id.myListView);
 
  int layoutID = android.R.layout.simple_list_item_1;
  aa = new ArrayAdapter<String>(this, layoutID, requesters);
  myListView.setAdapter(aa);
 
  Button okButton = (Button)findViewById(R.id.okButton);
  okButton.setOnClickListener(new View.OnClickListener() {
    public void onClick(View view) {
      respond(true, locationCheckBox.isChecked());
    }
  });
 
  Button notOkButton = (Button)findViewById(R.id.notOkButton);
  notOkButton.setOnClickListener(new View.OnClickListener() {
    public void onClick(View view) {
      respond(false, locationCheckBox.isChecked());
    }
  });
 
  Button autoResponderButton =
    (Button)findViewById(R.id.autoResponder);
  autoResponderButton.setOnClickListener(new View.OnClickListener() {
    public void onClick(View view) {
      startAutoResponder();
    }
  });
}
 
public void respond(boolean ok, boolean includeLocation) {}
 
private void startAutoResponder() {}

6. Create a Broadcast Receiver that will listen for incoming SMS messages. Start by creating a new static string variable to store the incoming SMS message intent action, and then create a new Broadcast Receiver as a variable in the EmergencyResponder Activity. The receiver should listen for incoming SMS messages and call the requestReceived method when it sees SMS messages containing the @string/querystring resource you defined in step 4.

public static final String SMS_RECEIVED =
  "android.provider.Telephony.SMS_RECEIVED";
 
BroadcastReceiver emergencyResponseRequestReceiver = 
  new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
      if (intent.getAction().equals(SMS_RECEIVED)) {
        String queryString = getString(R.string.querystring).toLowerCase();
 
        Bundle bundle = intent.getExtras();
        if (bundle != null) {
          Object[] pdus = (Object[]) bundle.get("pdus");
          SmsMessage[] messages = new SmsMessage[pdus.length];
          for (int i = 0; i < pdus.length; i++)
            messages[i] = 
              SmsMessage.createFromPdu((byte[]) pdus[i]);
 
          for (SmsMessage message : messages) {
            if (message.getMessageBody().toLowerCase().contains
               (queryString)) 
              requestReceived(message.getOriginatingAddress());
          }
        }
      }
    }
  };
 
public void requestReceived(String from) {}

7. Override the onResume and onPause methods to register and unregister the Broadcast Receiver created in step 6 when the Activity resumes and pauses, respectively:

@Override 
public void onResume() {
  super.onResume();
  IntentFilter filter = new IntentFilter(SMS_RECEIVED);
  registerReceiver(emergencyResponseRequestReceiver, filter);
}
 
@Override 
public void onPause() {
  super.onPause();
  unregisterReceiver(emergencyResponseRequestReceiver);
}

8. Update the requestReceived method stub so that it adds the originating number of each status request's SMS to the “requesters” Array List:

public void requestReceived(String from) {
  if (!requesters.contains(from)) {
    lock.lock();
    requesters.add(from);
    aa.notifyDataSetChanged();
    lock.unlock();
  }
}

9. The Emergency Responder Activity should now be listening for status request SMS messages and adding them to the List View as they arrive. Start the application and send SMS messages to the device or emulator on which it's running. When they've arrived, they should be displayed as shown in Figure 17.3.

10. Update the Activity to let users respond to these status requests. Start by completing the respond method stub you created in step 5. It should iterate over the Array List of status requesters and send a new SMS message to each. The SMS message text should be based on the response strings you defined as resources in step 4. Fire the SMS using an overloaded respond method (which you'll complete in the next step):

public void respond(boolean ok, boolean includeLocation) {
  String okString = getString(R.string.allClearText);
  String notOkString = getString(R.string.maydayText);
  String outString = ok ? okString : notOkString;
 
  ArrayList<String> requestersCopy =
    (ArrayList<String>)requesters.clone();
 
  for (String to : requestersCopy)
    respond(to, outString, includeLocation);
}
 
private void respond(String to, String response,
                     boolean includeLocation) {}

Figure 17.3

17.3

11. Complete the respond method to handle sending of each response SMS. Start by removing each potential recipient from the “requesters” Array List before sending the SMS. If you are responding with your current location, use the Location Manager to find it before sending a second SMS with your current position as both a raw longitude/latitude and a geocoded address:

public void respond(String to, String response,
                    boolean includeLocation) {
  // Remove the target from the list of people we
  // need to respond to.
  lock.lock();
  requesters.remove(to);
  aa.notifyDataSetChanged();
  lock.unlock();
 
  SmsManager sms = SmsManager.getDefault();
 
  // Send the message
  sms.sendTextMessage(to, null, response, null, null);
 
  StringBuilder sb = new StringBuilder();
 
  // Find the current location and send it
  // as SMS messages if required.
  if (includeLocation) {
    String ls = Context.LOCATION_SERVICE;
    LocationManager lm = (LocationManager)getSystemService(ls);
    Location l =
      lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);
 
    if (l == null)
      sb.append("Location unknown.");
    else {
      sb.append("I'm @:\n");
      sb.append(l.toString() + "\n");
 
      List<Address> addresses;
      Geocoder g = new Geocoder(getApplicationContext(),
                                Locale.getDefault());
      try {
        addresses = g.getFromLocation(l.getLatitude(),
                                      l.getLongitude(), 1);
        if (addresses != null) {
          Address currentAddress = addresses.get(0);
          if (currentAddress.getMaxAddressLineIndex() > 0) {
            for (int i = 0;
                 i < currentAddress.getMaxAddressLineIndex();
                 i++) {
              sb.append(currentAddress.getAddressLine(i));
              sb.append("\n");
            }
          }
          else {
            if (currentAddress.getPostalCode() != null)
              sb.append(currentAddress.getPostalCode());
          }
        }
      } catch (IOException e) {
        Log.e("SMS_RESPONDER", "IO Exception.", e);
      }
 
      ArrayList<String> locationMsgs =
        sms.divideMessage(sb.toString());
      for (String locationMsg : locationMsgs)
        sms.sendTextMessage(to, null, locationMsg, null, null);
    }
  }
}

12. In emergencies it's important that messages get through. Improve the robustness of the application by including auto-retry functionality. Monitor the success of your SMS transmissions so that you can rebroadcast a message if it doesn't successfully send.

12.1 Start by creating a new public static String in the Emergency Responder Activity to be used within Broadcast Intents to indicate the SMS has been sent.

public static final String SENT_SMS = 
  "com.paad.emergencyresponder.SMS_SENT";

12.2 Update the respond method to include a new PendingIntent that broadcasts the action created in the previous step when the SMS transmission has completed. The packaged Intent should include the intended recipient's number as an extra.

Intent intent = new Intent(SENT_SMS);
  intent.putExtra("recipient", to);
  PendingIntent sentPI =
    PendingIntent.getBroadcast(getApplicationContext(),
                               0, intent, 0);
  // Send the message
  sms.sendTextMessage(to, null, response, sentPI, null);

12.3 Implement a new Broadcast Receiver to listen for this Broadcast Intent. Override its onReceive handler to confirm that the SMS was successfully delivered; if it wasn't, put the intended recipient back onto the requester Array List.

private BroadcastReceiver attemptedDeliveryReceiver = new
BroadcastReceiver() {
  @Override
  public void onReceive(Context _context, Intent _intent) {
    if (_intent.getAction().equals(SENT_SMS)) {
      if (getResultCode() != Activity.RESULT_OK) {
        String recipient = _intent.getStringExtra("recipient");
        requestReceived(recipient);
      }
    }
  }
};

12.4 Finally, register and unregister the new Broadcast Receiver by extending the onResume and onPause handlers of the Emergency Responder Activity:

@Override 
public void onResume() {
  super.onResume();
  IntentFilter filter = new IntentFilter(SMS_RECEIVED);
  registerReceiver(emergencyResponseRequestReceiver, filter);
  IntentFilter attemptedDeliveryfilter = new IntentFilter(SENT_SMS);
  registerReceiver(attemptedDeliveryReceiver,
                   attemptedDeliveryfilter);
}
@Override 
public void onPause() {
  super.onPause();
  unregisterReceiver(emergencyResponseRequestReceiver);
  unregisterReceiver(attemptedDeliveryReceiver);
}

2.1

All code snippets in this example are part of the Chapter 12 Emergency Responder Part 1 project, available for download at www.wrox.com.

This example has been simplified to focus on the SMS-based functionality it is attempting to demonstrate. Keen-eyed observers should have noticed at least three areas where it could be improved:

· The Broadcast Receiver created and registered in steps 6 and 7 would be better registered within the manifest to allow the application to respond to incoming SMS messages even when it isn't running.

· The parsing of the incoming SMS messages performed by the Broadcast Receiver in steps 6 and 8 should be moved into a Service and executed on a background thread. Similarly, step 12, sending the response SMS messages, would be better executed on a background thread within a Service.

· The UI should be implemented using Fragments within Activities, with the UI optimized for tablet and smartphone layouts.

The implementation of these improvements is left as an exercise for the reader.

Automating the Emergency Responder

In the following example, you'll fill in the code behind the Setup Auto Responder button added in the previous example, to let the Emergency Responder automatically respond to status update requests.

1. Start by updating the application's string.xml resource to define a name for the SharedPreferences to use to save the user's auto-response preferences, and strings to use for each of its Views:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="app_name">Emergency Responder</string>
  <string name="allClearButtonText">I am Safe and Well
  </string>
  <string name="maydayButtonText">MAYDAY! MAYDAY! MAYDAY!
  </string>
  <string name="setupautoresponderButtonText">Setup Auto Responder</string>
  <string name="allClearText">I am safe and well. Worry not!
  </string>
  <string name="maydayText">Tell my mother I love her.
  </string>
  <string name="querystring">are you OK?</string>
  <string name="querylistprompt">These people want to know if you\'re ok</string>
  <string name="includelocationprompt">Include Location in Reply</string>
 
  <string
    name="user_preferences">com.paad.emergencyresponder.preferences
  </string>
  <string name="respondWithPrompt">Respond with</string>
  <string name="transmitLocationPrompt">Transmit location</string>
  <string name="autoRespondDurationPrompt">Auto-respond for</string>
  <string name="enableButtonText">Enable</string>
  <string name="disableButtonText">Disable</string>
</resources>

2. Create a new autoresponder.xml layout resource that will be used to lay out the automatic response configuration window. Include an EditText View for entering a status message to send, a Spinner View for choosing the auto-response expiry time, and a CheckBox View to let users decide whether they want to include their location in the automated responses:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/respondWithPrompt"/>
  <EditText
    android:id="@+id/responseText"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:hint="@string/respondWithPrompt"/>
  <CheckBox
    android:id="@+id/checkboxLocation"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/includelocationprompt"/>
  <TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/autoRespondDurationPrompt"/>
  <Spinner
    android:id="@+id/spinnerRespondFor"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:drawSelectorOnTop="true"/>
  <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <Button
      android:id="@+id/okButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/enableButtonText"/>
    <Button
      android:id="@+id/cancelButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/disableButtonText"/>
  </LinearLayout>
</LinearLayout>

3. Create a new res/values/arrays.xml resource, and create arrays to use for populating the Spinner:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string-array name="respondForDisplayItems">
    <item>- Disabled -</item>
    <item>Next 5 minutes</item>
    <item>Next 15 minutes</item>
    <item>Next 30 minutes</item>
    <item>Next hour</item>
    <item>Next 2 hours</item>
    <item>Next 8 hours</item>
  </string-array>
 
  <array name="respondForValues">
    <item>0</item>
    <item>5</item>
    <item>15</item>
    <item>30</item>
    <item>60</item>
    <item>120</item>
    <item>480</item>
  </array>
</resources>

4. Create a new AutoResponder Activity, populating it with the layout you created in step 1:

package com.paad.emergencyresponder;
 
import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.res.Resources;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.BroadcastReceiver;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;
 
public class AutoResponder extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.autoresponder);
  }
}

5. Update the onCreate method to get references to each of the controls in the layout and wire up the Spinner using the arrays defined in step 3. Create two new stub methods, savePreferences and updateUIFromPreferences, that will be updated to save the auto-responder settings to a namedSharedPreferences and apply the saved SharedPreferences to the current UI, respectively.

Spinner respondForSpinner;
CheckBox locationCheckbox;
EditText responseTextBox;
 
@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.autoresponder);
 
  respondForSpinner = (Spinner)findViewById(R.id.spinnerRespondFor);
  locationCheckbox = (CheckBox)findViewById(R.id.checkboxLocation);
  responseTextBox = (EditText)findViewById(R.id.responseText);
 
  ArrayAdapter<CharSequence> adapter = 
    ArrayAdapter.createFromResource(this,
      R.array.respondForDisplayItems, 
      android.R.layout.simple_spinner_item);
 
  adapter.setDropDownViewResource(
    android.R.layout.simple_spinner_dropdown_item);
  respondForSpinner.setAdapter(adapter);
  
  Button okButton = (Button) findViewById(R.id.okButton);
  okButton.setOnClickListener(new View.OnClickListener() {
    public void onClick(View view) {
      savePreferences();
      setResult(RESULT_OK, null);
      finish();
    }
  });
 
  Button cancelButton = (Button) findViewById(R.id.cancelButton);
  cancelButton.setOnClickListener(new View.OnClickListener() {
    public void onClick(View view) {
      respondForSpinner.setSelection(-1);
      savePreferences();
      setResult(RESULT_CANCELED, null);
      finish();
    }
  });
  
  // Load the saved preferences and update the UI
  updateUIFromPreferences();
}
 
private void updateUIFromPreferences() {}
private void savePreferences() {}

6. Complete the two stub methods from step 5. Start with updateUIFromPreferences; it should read the current saved AutoResponder preferences and apply them to the UI:

public static final String autoResponsePref = "autoResponsePref";
public static final String responseTextPref = "responseTextPref";
public static final String includeLocPref = "includeLocPref";
public static final String respondForPref = "respondForPref";
public static final String defaultResponseText = "All clear";
 
private void updateUIFromPreferences() {
  // Get the saves settings
  String preferenceName = getString(R.string.user_preferences);
  SharedPreferences sp = getSharedPreferences(preferenceName, 0);
 
  boolean autoRespond = sp.getBoolean(autoResponsePref, false);
  String respondText = sp.getString(responseTextPref, defaultResponseText);
  boolean includeLoc = sp.getBoolean(includeLocPref, false);
  int respondForIndex = sp.getInt(respondForPref, 0);
 
  // Apply the saved settings to the UI
  if (autoRespond)
    respondForSpinner.setSelection(respondForIndex);
  else
    respondForSpinner.setSelection(0);
 
  locationCheckbox.setChecked(includeLoc);
  responseTextBox.setText(respondText);
}

7. Complete the savePreferences stub to save the current UI settings to a Shared Preferences file:

private void savePreferences() {
  // Get the current settings from the UI
  boolean autoRespond = 
    respondForSpinner.getSelectedItemPosition() > 0;
  int respondForIndex = respondForSpinner.getSelectedItemPosition();
  boolean includeLoc = locationCheckbox.isChecked();
  String respondText = responseTextBox.getText().toString();
 
  // Save them to the Shared Preference file
  String preferenceName = getString(R.string.user_preferences);
  SharedPreferences sp = getSharedPreferences(preferenceName, 0);
 
  Editor editor = sp.edit();
  editor.putBoolean(autoResponsePref,
                    autoRespond);
  editor.putString(responseTextPref,
                   respondText);
  editor.putBoolean(includeLocPref,
                    includeLoc);
  editor.putInt(respondForPref, respondForIndex);
  editor.commit();
 
  // Set the alarm to turn off the autoresponder
  setAlarm(respondForIndex);
}
 
private void setAlarm(int respondForIndex) {}

8. The setAlarm stub from step 7 is used to create a new Alarm that fires an Intent when the auto responder expires, which should result in the auto responder being disabled. You'll need to create a new Alarm object and a BroadcastReceiver that listens for it before disabling the auto responder accordingly.

8.1 Start by creating the action String that will represent the Alarm Intent:

public static final String alarmAction =
  "com.paad.emergencyresponder.AUTO_RESPONSE_EXPIRED";

8.2 Create a new Broadcast Receiver instance that listens for an Intent that includes the action specified in step 8.1. When this Intent is received, it should modify the auto-responder settings to disable the automatic response.

private BroadcastReceiver stopAutoResponderReceiver 
  = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    if (intent.getAction().equals(alarmAction)) {
      String preferenceName = getString(R.string.user_preferences);
      SharedPreferences sp = getSharedPreferences(preferenceName, 0);
 
      Editor editor = sp.edit();
      editor.putBoolean(autoResponsePref, false);
      editor.commit();
    }
  }
};

8.3 Then complete the setAlarm method. It should cancel the existing alarm if the auto responder is turned off; otherwise, it should update the alarm with the latest expiry time.

PendingIntent intentToFire;
 
private void setAlarm(int respondForIndex) {
  // Create the alarm and register the alarm intent receiver.
 
  AlarmManager alarms = 
    (AlarmManager)getSystemService(ALARM_SERVICE);
 
  if (intentToFire == null) {
    Intent intent = new Intent(alarmAction);
    intentToFire = 
      PendingIntent.getBroadcast(getApplicationContext(),
                                 0,intent,0);
 
    IntentFilter filter = new IntentFilter(alarmAction);
 
    registerReceiver(stopAutoResponderReceiver, filter);
  }
 
  if (respondForIndex < 1)
    // If "disabled" is selected, cancel the alarm.
    alarms.cancel(intentToFire);
 
  else {
    // Otherwise find the length of time represented
    // by the selection and set the alarm to
    // trigger after that time has passed.
    Resources r = getResources();
    int[] respondForValues = 
      r.getIntArray(R.array.respondForValues);
    int respondFor = respondForValues [respondForIndex];
 
    long t = System.currentTimeMillis();
    t = t + respondFor*1000*60;
 
    // Set the alarm.
    alarms.set(AlarmManager.RTC_WAKEUP, t, intentToFire);
  }
}

9. That completes the AutoResponder. Before you can use it, however, you need to add it to your application manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.paad.emergencyresponder"
  android:versionCode="1"
  android:versionName="1.0" >
 
  <uses-permission android:name="android.permission.RECEIVE_SMS"/>
  <uses-permission android:name="android.permission.SEND_SMS"/>
  <uses-permission
    android:name="android.permission.ACCESS_FINE_LOCATION"/>
 
  <uses-sdk android:targetSdkVersion="15"/>
 
  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name" >
    <activity
      android:name=".EmergencyResponder"
      android:label="@string/app_name" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
    <activity
      android:name=".AutoResponder"
      android:label="Auto Responder Setup"
    />
  </application>
 
</manifest>

10. To enable the auto-responder, return to the Emergency Responder Activity and update the startAutoResponder method stub that you created in the previous example. It should open the AutoResponder Activity you just created.

private void startAutoResponder() {
  startActivityForResult(new Intent(EmergencyResponder.this,
                                    AutoResponder.class), 0);
}

11. If you start your project, you should now be able to bring up the Auto Responder Setup window to set the auto-response settings (see Figure 17.4).

Figure 17.4

17.4

12. The final step is to update the requestReceived method in the Emergency Responder Activity to check if the auto-responder has been enabled.

If it has, the requestReceived method should automatically execute the respond method, using the message and location settings defined in the application's Shared Preferences:

public void requestReceived(String from) {
  if (!requesters.contains(from)) {
    lock.lock();
    requesters.add(from);
    aa.notifyDataSetChanged();
    lock.unlock();
 
    // Check for auto-responder
    String preferenceName = getString(R.string.user_preferences);
    SharedPreferences prefs 
      = getSharedPreferences(preferenceName, 0);
 
    boolean autoRespond = prefs.getBoolean(AutoResponder.autoResponsePref, false);
 
    if (autoRespond) {
      String respondText = prefs.getString(AutoResponder.responseTextPref,
                                           AutoResponder.defaultResponseText);
      boolean includeLoc = prefs.getBoolean(AutoResponder.includeLocPref, false);
 
      respond(from, respondText, includeLoc);
    }
  }
}

2.1

All code snippets in this example are part of the Chapter 12 Emergency Responder Part 2 project, available for download at www.wrox.com.

You should now have a fully functional interactive and automated emergency responder.

Introducing SIP and VOIP

Session Initiation Protocol (SIP) is a signaling protocol used for managing communication sessions over IP connections—typically, voice (VOIP) and video calls.

SIP APIs were introduced in Android 2.3 (API level 9), allowing you to include Internet-based telephony features in your applications without needing to manage the underling client-side media and communications stack.

Android 4.0 (API level 14) introduced the capability for applications to add voice mail entries from their underlying services to the system. If you are planning to build your own SIP client, these new voice mail APIs provide a method for integrating messages left by callers seamlessly into the device's voice mail.

2.1

The instructions for building your own SIP client are beyond the scope of this book. You can find a detailed introduction to creating SIP clients—including a full working example—at the Android Developer site: http://developer.android.com/guide/topics/network/sip.html.