BroadcastReceiver + SMS_RECEIVED

I'd like my app to catch incoming SMS messages. There are a few examples of this around. Looks like we just need to do this:

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

// SMSReceiver.java
public class SMSReceiver extends BroadcastReceiver 
{ 
    @Override 
    public void onReceive(Context context, Intent intent) { 
        Log.i(TAG, "SMS received.");
        ....
    }
}

is this correct? I'm sending my phone some sms messages, but the log statement never gets printed. I do have some other SMS applications installed on the phone, which display a popup when the sms is received - are they somehow blocking the intent from getting passed down to my app, they are just consuming it completely?

Thanks


You would also need to specify a uses-permission in your manifest file:

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

The following tutorials should help:

React on incoming SMS
SMS messaging in Android


There are a few gotchas on the way. You can find all the needed info on stackoverflow. I have gathered all the info in this answer, for convenience.

Things to be noticed

  1. I assume android kitkat and above.
  2. The intent for incomming sms is "android.provider.Telephony.SMS_RECEIVED"
  3. You can change the priority of the intent filter, but it's not necessary.
  4. You need this permission "android.permission.RECEIVE_SMS" in manifest xml, in order to receive sms messages. In android 6 and above, you additionally need to ask for the permission in runtime.
  5. You do not need to set the MIME type of data in the intent filter. Intent filter should pass only on empty data if no MIME type is set, but fortunately it will still work without MIME.
  6. adb shell am broadcast will not work. Use telnet connection to simulator to test sms receiving.
  7. Long sms messages are divided into small sms chunks. We need to concatenate them.

How to send a sms message to the emulator

The most important thing is to have the possibility to send fake sms messages to the device, so we can test the code.

For this we will use a virtual device and a telnet connection to it.

  1. Create a virtual device in android studio and run the simulator
  2. Look at the title bar in the simulator window. There is the device name and a port number. We need to know this port number in the next steps.
  3. Now connect to the port number shown in the simulator title bar with telnet

     $ telnet localhost 5554
    
  4. If you see this: Android Console: Authentication required, then you need to authenticate the connection with this command:

     auth xxxxxx
    

    Replace the xxxxxx above with the token read from ~/.emulator_console_auth_token file.

  5. Now you should be able to run all the commands. To send a sms message, type this command:

     sms send 555 "This is a message"
    

    Where you can replace 555 with the sender telephone number and a message of your own.

How to listen to SMS_RECEIVED broadcasts

To get the broadcasts, you need to register a BroadcastReceiver object. You can do this in the manifest.xml OR just call registerReceiver function. I will show you the latter, as it is easier to reason about and yet more flexible.

Connecting the broadcast receiver with the main activity

The data flow is one way. From broadcast receiver to the main activity. So the simplest way to get them to talk is to use a function interface. The activity will implement such a function and the broadcast receiver will have the activity instance passed as a parameter in the constructor.

File SmsHandler.java:

package ...

interface SmsHandler {
    void handleSms(String sender, String message);
}

Implementing the broadcast receiver

The broadcast receiver will get the intent in a callback. We will use the function Telephony.Sms.Intents.getMessagesFromIntent(intent) to get the sms messages. Notice the SmsHandler parameter in the constructor. It will be the activity to which we will send the received sms.

File SmsInterceptor.java:

package ...

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.provider.Telephony;
import android.telephony.SmsMessage;

public class SmsInterceptor extends BroadcastReceiver {

    private SmsHandler handler;

    /* Constructor. Handler is the activity  *
     * which will show the messages to user. */
    public SmsInterceptor(SmsHandler handler) {
        this.handler = handler;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        /* Retrieve the sms message chunks from the intent */
        SmsMessage[] rawSmsChunks;
        try {
            rawSmsChunks = Telephony.Sms.Intents.getMessagesFromIntent(intent);
        } catch (NullPointerException ignored) { return; }

        /* Gather all sms chunks for each sender separately */
        Map<String, StringBuilder> sendersMap = new HashMap<>();
        for (SmsMessage rawSmsChunk : rawSmsChunks) {
            if (rawSmsChunk != null) {
                String sender = rawSmsChunk.getDisplayOriginatingAddress();
                String smsChunk = rawSmsChunk.getDisplayMessageBody();
                StringBuilder smsBuilder;
                if ( ! sendersMap.containsKey(sender) ) {
                    /* For each new sender create a separate StringBuilder */
                    smsBuilder = new StringBuilder();
                    sendersMap.put(sender, smsBuilder);
                } else {
                    /* Sender already in map. Retrieve the StringBuilder */
                    smsBuilder = sendersMap.get(sender);
                }
                /* Add the sms chunk to the string builder */
                smsBuilder.append(smsChunk);
            }
        }

        /* Loop over every sms thread and concatenate the sms chunks to one piece */
        for ( Map.Entry<String, StringBuilder> smsThread : sendersMap.entrySet() ) {
            String sender  = smsThread.getKey();
            StringBuilder smsBuilder = smsThread.getValue();
            String message = smsBuilder.toString();
            handler.handleSms(sender, message);
        }
    }
}

The main activity

Finally we need to implement SmsHandler interface into the main activity and add registering the broadcast receiver and permission check to the onCreate function.

File MainActivity.java:

package ...

import ...

public class MainActivity extends AppCompatActivity implements SmsHandler {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /* Register the broadcast receiver */
        registerSmsListener();

        /* Make sure, we have the permissions */
        requestSmsPermission();
    }

    /* This function will be called by the broadcast receiver */
    @Override
    public void handleSms(String sender, String message) {
        /* Here you can display the message to the user */
    }

    private void registerSmsListener() {
        IntentFilter filter = new IntentFilter();
        filter.addAction("android.provider.Telephony.SMS_RECEIVED");
        /* filter.setPriority(999); This is optional. */
        SmsInterceptor receiver = new SmsInterceptor(this);
        registerReceiver(receiver, filter);
    }

    private void requestSmsPermission() {
        String permission = Manifest.permission.RECEIVE_SMS;
        int grant = ContextCompat.checkSelfPermission(this, permission);
        if ( grant != PackageManager.PERMISSION_GRANTED) {
            String[] permission_list = new String[1];
            permission_list[0] = permission;
            ActivityCompat.requestPermissions(this, permission_list, 1);
        }
    }
}

Finally remember to add RECEIVE_SMS permission to your manifest xml

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
    <application>
        ...
    </application>
</manifest>

One more thing that these answers haven't mentioned - you should require the permission android.permission.BROADCAST_SMS. If you don't do this, any application can spoof messages in your app.

<receiver android:name=".SMSReceiver"
              android:exported="true"
              android:permission="android.permission.BROADCAST_SMS">
             <intent-filter>
                 <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
             </intent-filter>
 </receiver>