Send SMS from Android app using Nexmo and retrofit.

Send SMS from Android app using Nexmo and retrofit.

Hello everyone, this post is about how to use the Nexmo service and Retrofit libarary to programmatically send SMS from your Android App. This does not decrease the balance from the user’s SIM. We will be using the Retrofit library to make our network calls to the Nexmo services. This post is not so beginner friendly and I expect you have  some knowledge of how RESTful web services and networking in Android works.

If you would like to have a look at the source code right away, click this link . Just follow the instructions over there to download the app, run in your Android Studio. And you should be able to test the app. If you could not setup the app from there, do reach out on my twitter handle @ravigarbuja and I will try and help you. The app wont work unless you have the api key and shared secret from nexmo.com put into  Config.java.

For those of you who want to read this blog post and follow along, please continue reading.

Step 1:

Now the first step is to sign up for Nexmo account. You can do so by following this link. I also recommend you have Android Studio 3.+ and gradle 4.+ on your development environment.

We will be using the Retrofit library to hit the Nexmo API. Retrofit is a network request library for android and has many features to ease the development of your app, that interacts with RESTful services of some sort.

Now, lets jump into the code, shall we??

Step 2:

Create a new android studio project and edit the app/build.gradle , add the following lines into your  app level build.gradle
//Retrofit
compile 'com.squareup.retrofit2:retrofit:2.3.0'
//Gson Converter
compile 'com.squareup.retrofit2:converter-gson:2.3.0'

These lines need to be inside of dependencies{ } . These are required for us to use Retrofit classes and Gson Converter in our app.

Step 3:

Then open you `AndroidManifest.xml` file which is inside the Manifest folder and include the following line

 before the  tag to allow the app to access the internet.

Step 4:

Now let us build a basic UI for the app, in our activity_main.xml that will take input from the user for the phone number that we will be sending an SMS to and the actual text message. Finally there will be a button to send the message.





    

    


    

There you have it, the code for out UI. It has exactly two EditTexts to take number and the message from user, a button to initiate hitting the Nexmo API. The TextView on the last is for the response that we get after the api is hit.

Step 5:

Now that we have the UI, lets head to java code that will interact with the API endpoint at https://rest.nexmo.com/ For interacting with the API, we will have a java class and an interface.

The java class that provides a client for the app to interact with the nexmo RESTful services is ApiClient.java  which is given below:

public class ApiClient {
    public static final String BASE_URL = "https://rest.nexmo.com/";
    private static Retrofit retrofit = null;

    public static Retrofit getClient() {
        //Increase the default timeout time
        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(100, TimeUnit.SECONDS)
                .readTimeout(100, TimeUnit.SECONDS).build();
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

This returns an instance of the Retrofit object which has the base url, OkHttpClient and the GsonConverter factory within it. We will be using this object later on in our  MainActivity . As you can also see we have the base url here in our ApiClient class.

Step 6:

Now let us make an interface, which we will use to make request to the api endpoint used to send sms. Create new java class, select type of the class as interface, name it ApiInterface.java  and include the following code there.

public interface ApiInterface {

    @FormUrlEncoded
    @POST("sms/json")
    Call getMessageResponse(
            @Field("api_key") String apiKey,
            @Field("api_secret") String apiSecret,
            @Field("from") String from,
            @Field("to") String to,
            @Field("text") String text
    );
}

We use this interface to specify things like: what the request type is, what things are passed onto the request body and header and what the endpoint is. The base Url may be https://rest.nexmo.com/ but the endpoint is https://rest.nexmo.com/sms/json. Hence we need to specify here that we will be posting o the sms/json endpoint. The json  format here is according to the Nexmo api docs. The format can also be XML. The @FormUrlEncoded annotation indicates that we will be using FormUrlEncoded method to post. Retrofit uses these annotations which I think are very clean.

As you can see we will be needing the api_key and api_secret to make a successful request to the Nexmo API to send an SMS to a number. Also note that, We will only be able to send SMS to test numbers until we make a payment for Nexmo services. You can add test numbers after you have created a nexmo account from this link .

Step 7:

Next we need to create a POJO (Plain Old Java Object classes to be used to set the response that we get after hitting the api, the JSON format that we get from the Nexmo API is:

{
  "message-count": 1,
  "messages": [
    {
      "to": "447700900000",
      "message-id": "0A0000000123ABCD1",
      "status": "0",
      "remaining-balance": "3.14159265",
      "message-price": "0.03330000",
      "network": "12345"
    }
  ]
}

Now we have two POJO classes that represent exactly this. MessageResponse.java that represent the outer curly braces object. The MessageResponse has an array of Message objects.

public class MessageResponse {

    @SerializedName("message-count")
    private String messageCount;

    @SerializedName("messages")
    private Message[] messages;

    public String getMessageCount ()
    {
        return messageCount;
    }

    public void setMessageCount (String messageCount)
    {
        this.messageCount = messageCount;
    }

    public Message[] getMessages ()
    {
        return messages;
    }

    public void setMessages (Message[] messages)
    {
        this.messages = messages;
    }

    @Override
    public String toString()
    {
        return "ClassPojo [message-count = "+messageCount+", messages = "+messages+"]";
    }
}

And

public class Message {
    @SerializedName("to")
    private String to;

    @SerializedName("message-price")
    private String messagePrice;

    @SerializedName("status")
    private String status;

    @SerializedName("message-id")
    private String messageId;

    @SerializedName("remaining-balance")
    private String remainingBalance;

    private String network;

    public String getTo ()
    {
        return to;
    }

    public void setTo (String to)
    {
        this.to = to;
    }


    public String getStatus ()
    {
        return status;
    }

    public void setStatus (String status)
    {
        this.status = status;
    }



    public String getNetwork ()
    {
        return network;
    }

    public void setNetwork (String network)
    {
        this.network = network;
    }

    public String getMessagePrice() {
        return messagePrice;
    }

    public void setMessagePrice(String messagePrice) {
        this.messagePrice = messagePrice;
    }

    public String getMessageId() {
        return messageId;
    }

    public void setMessageId(String messageId) {
        this.messageId = messageId;
    }

    public String getRemainingBalance() {
        return remainingBalance;
    }

    public void setRemainingBalance(String remainingBalance) {
        this.remainingBalance = remainingBalance;
    }

    @Override
    public String toString() {
        return "Message{" +
                "to='" + to + '\'' +
                ", messagePrice='" + messagePrice + '\'' +
                ", status='" + status + '\'' +
                ", messageId='" + messageId + '\'' +
                ", remainingBalance='" + remainingBalance + '\'' +
                ", network='" + network + '\'' +
                '}';
    }
}

These two classes have getters and setters for the data members within them. Retrofit uses the setters to set data to the object on which we will be able to use getters to get the data. It all makes sense doesn’t it? The @SerializedName annotation is used by the retrofit to determine what the name of the key is when ther response is received.

Step 8:

Now lets jump into the MainActivity.

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final String TAG = MainActivity.class.getSimpleName();

    EditText toET, messageET;
    TextView messageAreaTV;
    Button sendButton;
    String FROM_NUMBER = "TestApp", TO_NUMBER = "", MESSAGE = "";
    String displayResult;

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

        initComponents();


        sendButton.setOnClickListener(this);

    }

    private void initComponents() {
        toET = findViewById(R.id.et_to_number);
        messageET = findViewById(R.id.et_message);
        sendButton = findViewById(R.id.btn_send);
        messageAreaTV = findViewById(R.id.tv_msg_area);
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        switch (id) {
            case R.id.btn_send:

                showShortToast("Send button clicked");

                getValuesFromETs();

                if (validateAllFields()) {
                    makeSendSMSApiRequest(FROM_NUMBER, TO_NUMBER, MESSAGE);
                } else {
                    showShortToast("Validation failed");
                }

                break;

            default:
                Log.d(TAG, " I am in default onClick");

        }
    }

    private void makeSendSMSApiRequest(String fromNumber, String toNumber, String message) {
        ApiInterface sendSMSapiInterface =
                ApiClient.getClient().create(ApiInterface.class);

        Call call = sendSMSapiInterface.getMessageResponse(Config.ApiKey, Config.ApiSecret,
                fromNumber, toNumber, message);
        call.enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) {
                try {
                    Log.d(TAG, String.valueOf(response.code()));
                    if (response.code() == 200) {
                        Log.d(TAG, response.body().toString());
                        Log.d(TAG, response.body().getMessages().toString());
                        Log.d(TAG, response.body().getMessageCount());
                        for (int i = 0; i < response.body().getMessageCount().length(); i++) {
                            Log.d(TAG, response.body().getMessages()[i].getTo());
                            Log.d(TAG, response.body().getMessages()[i].getMessageId());
                            Log.d(TAG, response.body().getMessages()[i].getStatus());
                            Log.d(TAG, response.body().getMessages()[i].getRemainingBalance());
                            Log.d(TAG, response.body().getMessages()[i].getMessagePrice());
                            Log.d(TAG, response.body().getMessages()[i].getNetwork());

                            displayResult = "TO: " + response.body().getMessages()[i].getTo() + "\n"
                                    + "Message-id: " + response.body().getMessages()[i].getMessageId() + "\n"
                                    + "Status: " + response.body().getMessages()[i].getStatus() + "\n"
                                    + "Remaining balance: " + response.body().getMessages()[i].getRemainingBalance() + "\n"
                                    + "Message price: " + response.body().getMessages()[i].getMessagePrice() + "\n"
                                    + "Network: " + response.body().getMessages()[i].getNetwork() + "\n\n";

                        }

                        messageAreaTV.setText(displayResult);

                    }

                } catch (Exception e) {
                    e.printStackTrace();
                    Log.e(TAG, e.getLocalizedMessage());
                }
            }

            @Override
            public void onFailure(Call call, Throwable t) {
                Log.e(TAG, t.getLocalizedMessage());
                showShortToast("onFailure");
            }
        });
    }

    private boolean validateAllFields() {
        if (!Patterns.PHONE.matcher(TO_NUMBER).matches()) {
            toET.setError("Please enter valid number");
            return false;
        } else if (MESSAGE.length() == 0) {
            messageET.setError("The message is empty");
            return false;
        } else {
            return true;
        }
    }

    private void getValuesFromETs() {
        TO_NUMBER = toET.getText().toString().trim();
        MESSAGE = messageET.getText().toString().trim();
    }

    public void showShortToast(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }
}

The components from the view have been initiated in the initComponents() method. Also the MainActivity implements View.OnClickListener so, if some item on the screen has setOnClickListener on it, the click event is triggered on the @Override public void onClick(View v) { }  method.

Explanation:

As you can see there is a little validation going on in the onClick method, which checks to see if the phone number input matches the Android PHONE pattern and if the message is empty or not.

If the validation is passed, the makeSendSMSapiRequest  (I know its not a great name) is called.

The method has:

 ApiInterface sendSMSapiInterface = ApiClient.getClient().create(ApiInterface.class);
 Call call = sendSMSapiInterface.getMessageResponse(Config.ApiKey, Config.ApiSecret,
                              fromNumber, toNumber, message);
 call.enqueue(new Callback() {

This is used to get an instance of the interface we defined earlier and pass the apikey, apisecret , fromNumber , toNumber and message as POST body. I read the Nexmo docs and they are quite contradictory to this method or I could be completely wrong. But the docs say that we need to pass api key and api secret as Query parameters. Correct me if I am wrong but aren’t the query parameters passed onto as the URL parameters like https://api.sth.com?q=sth&q=sth . But anyway I tested doing so from Postman and could not make a request. The app is working fine with passing everything as FormUrl data.

Anyhow, an instance of APiInterface is created and a call for MessageResponse, our POJO/ model is made using the ApiInterface instance. The call is enqueued which I believe I read is an Asynchronous method of Retrofit.

The other thing to notice here is I am passing the ApiKey and ApiSecret from another class named Config.java which is nothing but a class with two data members (Strings).

public class Config{
    //Obtain these at https://www.nexmo.com/
    public static final String ApiKey = "";
    public static final String ApiSecret = "";
}

Acquire your api key and api secret from nexmo.com after signup.

This gets us a new Callback for MessageResponse which is handled in on the onResponse() and onFailure() method as:

  @Override
            public void onResponse(Call call, Response response) {
                try {
                    Log.d(TAG, String.valueOf(response.code()));
                    if (response.code() == 200) {
                        Log.d(TAG, response.body().toString());
                        Log.d(TAG, response.body().getMessages().toString());
                        Log.d(TAG, response.body().getMessageCount());
                        for (int i = 0; i < response.body().getMessageCount().length(); i++) {
                            Log.d(TAG, response.body().getMessages()[i].getTo());
                            Log.d(TAG, response.body().getMessages()[i].getMessageId());
                            Log.d(TAG, response.body().getMessages()[i].getStatus());
                            Log.d(TAG, response.body().getMessages()[i].getRemainingBalance());
                            Log.d(TAG, response.body().getMessages()[i].getMessagePrice());
                            Log.d(TAG, response.body().getMessages()[i].getNetwork());

                            displayResult = "TO: " + response.body().getMessages()[i].getTo() + "\n"
                                    + "Message-id: " + response.body().getMessages()[i].getMessageId() + "\n"
                                    + "Status: " + response.body().getMessages()[i].getStatus() + "\n"
                                    + "Remaining balance: " + response.body().getMessages()[i].getRemainingBalance() + "\n"
                                    + "Message price: " + response.body().getMessages()[i].getMessagePrice() + "\n"
                                    + "Network: " + response.body().getMessages()[i].getNetwork() + "\n\n";

                        }

                        messageAreaTV.setText(displayResult);

                    }

                } catch (Exception e) {
                    e.printStackTrace();
                    Log.e(TAG, e.getLocalizedMessage());
                }
            }

            @Override
            public void onFailure(Call call, Throwable t) {
                Log.e(TAG, t.getLocalizedMessage());
                showShortToast("onFailure");
            }
});

As the names clearly suggest , they are triggered on response and on failure respectively. The Log.d messages are displayed in the D channel in your Android Studio logcat. Have a look at them if you get an error of some sort.

I hope you got an understanding of what the code is doing and you could test the app for yourself. But if you could not, contact me @ravigarbuja in twitter and I will see if I can help.

The app is not bulletproof in any sense, since the validation function is not the best. There may be cases where internet connection may be slow or the phone number provided might not get normalized by the Nexmo(although it does a very good job in that). So you may not get expected results in those and some other cases I have not been able to think.

Thanks. Happy Coding!!!