August 1, 2017 / Reading time: 6m

Detecting & sending SMS on Android 📮

android

opensource

Runtime permissions, BroadcastReceiver & SmsManger.

Motivation

With so many different ways to communicate (I’m 👀 at you Google Developers), one might find odd talking about integrating the old-school SMS service in your app. Truth is, besides the usual 2-step-verification and related services, there are some areas on the planet 🌍 where data is still rare and/or very expensive, so if we are to tackle the next billion, most of these new users are in emerging markets so, SMS might be a good alternative or at least something to consider.

TL;DR: Project is available on GitHub. Note & Overview👇 gives you the rest.

With relative low effort you can have your app using SMS as a data source while providing a pleasing UX, a material design UI (beats plain text👌).

Note: I’m using the Bus-eta project as a implementation example, you can read more about the it in my previous post. I’ve also created a simplified and easier to understand project, check the available on GitHub.

Overview

  1. Ask for SMS permissions (both manifest and runtime)
  2. Send SMS using the system’s SmsManager
  3. Setting up a BroadcastReceiver to listen and handle for incoming SMS.

1. Asking for permission

We’ll need to ask for some permissions in order for our app to work with SMS, start by declaring in your AndroidManifest

<!-- To use SMS based services -->
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />

Now our pre-M users will be asked to accept these permission when they choose to install the app through the PlayStore.


But, as of beginning in Android 6.0 (API level 23), permissions must be requested at runtime, so we still need to make our app Marshmallow ready, otherwise a big % of users won’t be able to use sms, resulting in a very useless and frustrating effort from our part.

Sending SMS when you don’t ask for permissions in Marshmallow

So whenever you need to use SMS, check if the app has permissions if not, request:

// Activity

/**
 * Check if we have SMS permission
 */
public boolean isSmsPermissionGranted() {
  return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_GRANTED;
}

/**
 * Request runtime SMS permission
 */
private void requestReadAndSendSmsPermission() {
  if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_SMS)) {
    // You may display a non-blocking explanation here, read more in the documentation:
    // https://developer.android.com/training/permissions/requesting.html
  }
  ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_SMS}, SMS_PERMISSION_CODE);
}

It’s important to not request beforehand, I personally hate it when I start a new app and I get asked for random permissions, sometimes I haven’t even used the app, so don’t be lazy, and only ask when needed.

Requesting the permission will present the user with the follow system AlertDialog:

Transport ETA requesting runtime SMS permission.

As you can see the message given by the system is quite strong, unless it’s straightforward to your users why your app would need to access SMS, I would recommend you to show a simple explanation beforehand to avoid some of your more paranoid users (like me) to deny and render the whole feature useless.

Give the user some some context, is simple:

// Activity


/**
 * Displays an AlertDialog explaining the user why the SMS permission is going to be requests
 *  
 * @param makeSystemRequest if set to true the system permission will be shown when the dialog is dismissed.
 */
public void showRequestPermissionsInfoAlertDialog() {
  showRequestPermissionsInfoAlertDialog(true);
}

public void showRequestPermissionsInfoAlertDialog(final boolean makeSystemRequest) {
  AlertDialog.Builder builder = new AlertDialog.Builder(this);
  builder.setTitle(R.string.permission_alert_dialog_title); // Your own title
  builder.setMessage(R.string.permission_dialog_message); // Your own message

  builder.setPositiveButton(R.string.action_ok, new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialog, int which) {
      dialog.dismiss();
       // Display system runtime permission request?
       if (makeSystemRequest) {
         requestReadAndSendSmsPermission(this);
       }
    }
  });
	
  builder.setCancelable(false);
  builder.show();
}
Showing a simple explanation beforehand

With minimal effort, our users are presented with simple but crucial information. Now they know why they should allow the app to access such sensitive data.


After the user decides we need to handle the permission response:

// Activity

@Override
public void onRequestPermissionsResult(int requestCode,
        String permissions[], int[] grantResults) {
    switch (requestCode) {
        case SMS_PERMISSION_CODE: {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                // permission was granted, yay! Do the
                // SMS related task you need to do.

            } else {
                // permission denied, boo! Disable the
                // functionality that depends on this permission.
            }
            return;
        }
        // other 'case' lines to check for other
        // permissions this app might request
    }
}

Note: Similar to calling startActivityForResult() , the SMS_PERMISSION_CODE is just a constant variable, it should match for both asking the permission, and handling the response.

2. Sending SMS through our app

Now that we have the required permissions we’ll use the default SmsManager in order to send our text:

SmsManager.getDefault().sendTextMessage(number, null, smsText, null, null);

Voilá! Our App will use the device’s default sms-messenger-app to send the sms 🙂 👌.

It’s considered good practice to inform the user normal fees apply, our app is just a dolled up version of the usual/default process. 💸

3. Listening for incoming SMS

Nowadays apps rarely have a one-way communication flow, so in order to listen to incoming SMS’s we need to setup our custom BroadcastReceiver

/**
 * A broadcast receiver who listens for incoming SMS
 */

public class SmsBroadcastReceiver extends BroadcastReceiver {

	private static final String TAG = "SmsBroadcastReceiver";

	private final String serviceProviderNumber;
	private final String serviceProviderSmsCondition;

	private Listener listener;

	public SmsBroadcastReceiver(String serviceProviderNumber, String serviceProviderSmsCondition) {
		this.serviceProviderNumber = serviceProviderNumber;
		this.serviceProviderSmsCondition = serviceProviderSmsCondition;
	}

	@Override
	public void onReceive(Context context, Intent intent) {
		if (intent.getAction().equals(Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
			String smsSender = "";
			String smsBody = "";
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
				for (SmsMessage smsMessage : Telephony.Sms.Intents.getMessagesFromIntent(intent)) {
					smsSender = smsMessage.getDisplayOriginatingAddress();
					smsBody += smsMessage.getMessageBody();
				}
			} else {
				Bundle smsBundle = intent.getExtras();
				if (smsBundle != null) {
					Object[] pdus = (Object[]) smsBundle.get("pdus");
					if (pdus == null) {
						// Display some error to the user
						Log.e(TAG, "SmsBundle had no pdus key");
						return;
					}
					SmsMessage[] messages = new SmsMessage[pdus.length];
					for (int i = 0; i < messages.length; i++) {
						messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
						smsBody += messages[i].getMessageBody();
					}
					smsSender = messages[0].getOriginatingAddress();
				}
			}

			if (smsSender.equals(serviceProviderNumber) && smsBody.startsWith(serviceProviderSmsCondition)) {
				if (listener != null) {
					listener.onTextReceived(smsBody);
				}
			}
		}
	}

	void setListener(Listener listener) {
		this.listener = listener;
	}

	interface Listener {
		void onTextReceived(String text);
	}
}

Of course you could always create a simple BroadcastReceiver and have the logic present on your Activity, but I prefer to keep this wrapper under a custom SmsBroadcastReceiver and only passing a listener for when the data is received. Avoiding ugly code floating around and unnecessary coupling.


Looking at line 47 of the above gist you can see that I’ve implemented some stricter conditions to when the listener is called, this suits my specific case, I only want to detect sms that come from a specific number and have a starting pattern of “SMS@CARRIS” which is passed via constructor:

private final String serviceProviderNumber;

private final String serviceProviderSmsCondition;

This way we may ignore any other incoming sms, now we need to register ou receiver, in my case I want to listen while the app is open, so I’ll register in my Application class

public class App extends Application {

	private SmsBroadcastReceiver smsBroadcastReceiver;

	@Override
	public void onCreate() {
		super.onCreate();
		smsBroadcastReceiver = new SmsBroadcastReceiver(BuildConfig.SERVICE_NUMBER, BuildConfig.SERVICE_CONDITION);
		registerReceiver(smsBroadcastReceiver, new IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION));
	}

	@Override
	public void onTerminate() {
		unregisterReceiver(smsBroadcastReceiver);
		super.onTerminate();
	}
}

The final step is assigning a listener, will only be called if the conditions are met:

smsBroadcastReceiver.setListener(new SmsBroadcastReceiver.Listener( {
   @Override
   public void onTextReceived(String text) {
      // Do stuff with received text!
   }
});

You can also register your broadcast receiver via the manifest by adding the following line to your AndroidManifest:

<receiver android:name="SmsBroadcastReceiver" />

And handle all the logic inside your own BroadcasReceiver, this way you keep all of your business logic wrapped inside your own class. For my case I want to have more control and adaptability, but using the manifest is the same as registering in your Application. My actual project has a Controller which handles a lot of the underlying edge cases that may surface with these custom implementations, but are outside of the article’s scope.

While using BroadcastReceivers beware of the priority setting, (but don’t abuse it 🙌).

References:

Share