Sometimes we need to automate the OTP verification process in our mobile app for better user experience. For a smooth experience for this. google introduced it’s new SMS Retrieval API. Here in this article we will learn how to implement this in our app.
If you’ve used Uber or WhatsApp, you would have noticed that their SMS verification is automatic: The user need not manually type in the verification code. This makes their on-boarding feel seamless and kind of effortless on the part of the user. And the best part of this is that they do not request the user’s SMS permission. This is done using Google’s SMS Retrieval API, and in this article, we will see how to achieve just that in our apps as well.
Before we begin, we need to import the following libraries:
Google Play Services Auth API
– This library contains theSMS Retrieval API
classes.Apache Commons
– This will be used to parse out the verification code from the SMS message.EventBus
– We will be using a BroadcastReceiver to listen for the retrieved SMS from theSMS Retrieval API
. EventBus is a library that uses the publisher/subscriber pattern and will be used to simplify the communication between ourBroadcastReceiver
and ourActivity
classes.
Let’s add them to our app’s build.gradle
file:
implementation 'com.google.android.gms:play-services-auth:19.0.0'
implementation 'org.apache.commons:commons-lang3:3.11'
implementation 'org.greenrobot:eventbus:3.2.0'
Alright, we are set.
Our XML layout file is extremely simple. It contains a TextView
in the middle of the screen. This view will be used to display the verification code we extract using the SMS Retrieval API
.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/otp_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@android:color/black"
android:textSize="30sp"
android:gravity="center"
android:text="Loading..."
/>
</RelativeLayout>
What we are going to do next is to get an instance of the SmsRetrieverClient
object, call its startSmsRetriever
instance method, and attach onSuccess and onFailure Listeners to the resulting Task
. I’m going to wrap that portion of code in a method to easily reuse later:
private fun startSmsListener() {
smsRetrieverClient.startSmsRetriever()
.addOnSuccessListener {
Toast.makeText(this@MainActivity, "Starting Sms Retriever", Toast.LENGTH_SHORT).show()
}.addOnFailureListener { e ->
e.printStackTrace()
Toast.makeText(this@MainActivity, e.message, Toast.LENGTH_SHORT).show()
}
}
Then we call that method immediately after onCreate
:
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var smsRetrieverClient: SmsRetrieverClient
private lateinit var otpTextView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
otpTextView = findViewById(R.id.otp_text_view)
smsRetrieverClient = SmsRetriever.getClient(this)
startSmsListener()
}
...
At this point, when an SMS message is received on our device, the Play Services library we added earlier will broadcast an SmsRetriever.SMS_RETRIEVED_ACTION
intent to our app. This intent contains the status of the background processing as well as the text of the SMS message.
Let’s create a BroadcastReceiver class to handle this:
SmsBroadcastReceiver.kt
class SmsBroadcastReceiver : BroadcastReceiver() {
companion object {
val TAG = SmsBroadcastReceiver::class.java.simpleName
}
override fun onReceive(context: Context?, intent: Intent) {
Log.d(TAG, "onReceive Called!")
if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) {
val bundle = intent.extras
if (bundle != null) {
val status: Status = bundle[SmsRetriever.EXTRA_STATUS] as Status
var isTimeout = false
var smsMessage: String? = null
when (status.statusCode) {
CommonStatusCodes.SUCCESS -> {
val message =
bundle[SmsRetriever.EXTRA_SMS_MESSAGE] as String
Log.d(TAG, message)
smsMessage = message
}
CommonStatusCodes.TIMEOUT -> {
Log.d(TAG, "Timeout")
isTimeout = true
}
}
EventBus.getDefault().post(SmsRetrievedEvent(isTimeout, smsMessage))
}
}
}
}
In the BroadcastReceiver’s onReceive
method, we first check the Status
of the background processing done by the SMS Retrieval API
. We also create an instance of a class called SmsRetrievedEvent
. This is our event class that will be published by EventBus
to our Subscriber
.The SmsRetrievedEvent
class is nothing but a simple Kotlin data class.
SmsRetrievedEvent.kt
data class SmsRetrievedEvent(
val isTimeout: Boolean,
val smsMessage: String?
)
If the background processing was successful, we set the message property of the SmsRetrievedEvent
class to the SMS message we’ve retrieved. Otherwise, we set timeout to true if a timeout occurred. We finally publish the event to any listening subscriber.
Note: Timeouts occurs if messages are not handled within 5 minutes after processing by the SMS Retreival API
.
Let’s not forget to register this BroadcastReceiver
in our AndroidManifest
file:
AndroidManifest.xml
<application>
...
<receiver
android:name=".SmsBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.auth.api.phone.SMS_RETRIEVED"/>
</intent-filter>
</receiver>
</application>
Next, we are going to register, unregister as well as implement our subscriber in our MainActivity
class. This method will be defined with the @Subscribe
annotation and will be called when an event is published/posted.
Note: Always remember to unregister your subscribers to avoid memory leak issues.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var smsRetrieverClient: SmsRetrieverClient
private lateinit var otpTextView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
otpTextView = findViewById(R.id.otp_text_view)
smsRetrieverClient = SmsRetriever.getClient(this)
startSmsListener()
}
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}
override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
}
@Subscribe
fun onEvent(event: SmsRetrievedEvent) {
}
private fun startSmsListener() {
smsRetrieverClient.startSmsRetriever()
.addOnSuccessListener {
Toast.makeText(this@MainActivity, "Starting Sms Retriever", Toast.LENGTH_SHORT).show()
}.addOnFailureListener { e ->
e.printStackTrace()
Toast.makeText(this@MainActivity, e.message, Toast.LENGTH_SHORT).show()
}
}
}
Finally, we parse out the 4 digit verification code from the message using the substringAfterLast
method from the Apache Commons
library, and display it in our TextView
...
@Subscribe
fun onEvent(event: SmsRetrievedEvent) {
Log.d("SMS EVENT", "RECEIVED SMS EVENT")
Log.d("SMS EVENT - Timeout=", event.isTimeout.toString())
Log.d("SMS EVENT - Message=", if(!event.isTimeout) event.smsMessage!! else "Timeout: No Message")
val otp: String =
StringUtils.substringAfterLast(event.smsMessage, "is").replace(":", "")
.trim().substring(0, 4)
Log.d("EVENT", otp)
runOnUiThread {
otpTextView.text = if (!event.isTimeout) otp else "Timeout :("
}
startSmsListener()
}
...
One important thing to keep in the back of our minds:
The SMS Retreival API
requires that SMS messages be in the following format:
Your verification code is 9872
FA+9qCX9VSu
The SMS message must end with an 11-character hash string that identifies your app(which is FA+9qCX9VSu
in this case). Click here to see how to compute your app’s hash.
At this point we can send the SMS message to our device, extract and send the 4 digit code to our server for verification.