Android auto-populate OTP in Kotlin

Chris Chen
3 min readAug 13, 2019

Google prohibits the SMS/CALL_LOG related permission if your app is not mainly for the phone call, text purpose. Or you cannot send the updated app via Google Play anymore. But for some app, we need to verify the user’s account via their phone. And in the user’s perspective, we should make it as much as easier to do.

There is an alternative way— SMS Retrieve API

1. You need to add one auth dependency.

dependencies {
//... skip above dependencies
"com.google.android.gms:play-services-auth:17.0.0"
}

2. Add request hint to your Activity or Fragment. The best way is to use the hint picker pop up. The user can choose the phone numbers stored on the device. Then the user doesn’t need to input the phone manually. Like the below RequestHintActivity sample:

class RequestHintActivity : AppCompatActivity() {

private val TAG = RequestHintActivity::class.java.simpleName

private val REQUEST_PHONE_NUMBER = 5566

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestHint()
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_PHONE_NUMBER && resultCode == Activity.RESULT_OK) {
val fullPhoneNumber = data?.getParcelableExtra<Credential>(Credential.EXTRA_KEY)?.id
// call api or do something
}
}

private fun requestHint() {

val hintRequest = HintRequest.Builder()
.setPhoneNumberIdentifierSupported(true)
.build()

val intent = Auth.CredentialsApi.getHintPickerIntent(
GoogleApiClient.Builder(this)
.addApi(Auth.CREDENTIALS_API)
.build(), hintRequest)

try {
startIntentSenderForResult(intent.intentSender,
REQUEST_PHONE_NUMBER, null, 0, 0, 0, null)
} catch (e: IntentSender.SendIntentException) {
Log.e(TAG, e.toString())
}

}
}

3. Backend side will implement the SMS message like this. The last line shows the hash string encrypts by your App key store. This is for android system know which app needs to receive the SMS message. How to generate the hash string can see here. (Be aware of that maybe you have staging and production. The key might be different.)

<#> 123456 is your ExampleApp code ...FA+9qCX9VSu

4. Add one SMSBroadcastReceiver to receive the SMS message. Then we can register the SMS receiver dynamically.

class SMSBroadcastReceiver(private val listener: Listener) : BroadcastReceiver() {

companion object {
private val TAG = SMSBroadcastReceiver::class.java.simpleName
}

override fun onReceive(context: Context?, intent: Intent?) {
if (SmsRetriever.SMS_RETRIEVED_ACTION == intent?.action) {
val extras = intent.extras
val status = extras?.get(SmsRetriever.EXTRA_STATUS) as? Status

when (status?.statusCode) {
CommonStatusCodes.SUCCESS -> {
val message = extras.get(SmsRetriever.EXTRA_SMS_MESSAGE)
if (message is String) {
listener.onRetrievedMessageSuccess(parseSMSCode(message))
}
}
CommonStatusCodes.TIMEOUT -> {
listener.onRetrievedMessageFailed()
Log.e(TAG, "SmsRetriever timeout")
}
else -> {
listener.onRetrievedMessageFailed()
}
}
}
}
// This method is depends on your SMS format. The purpose is to extract the code.
private fun parseSMSCode(message: String): String {
return message.replace("<#> ", "").substring(0, 6)
}
interface Listener {
fun onRetrievedMessageSuccess(smsCode: String)

@JvmDefault
fun onRetrievedMessageFailed() {}
}
}

For parseSMSCode , I will suggest you write some test to make sure the parse method is working properly.

5. Add startSMSRetrieve method in your Activity/Fragment to start SMS retriever.

class SmsReceiveActivity : AppCompatActivity(), SMSBroadcastReceiver.Listener {
override fun onRetrievedMessageSuccess(smsCode: String) {
// call api or something
}

private val TAG = SmsReceiveActivity::class.java.simpleName

private lateinit var broadcastReceiver: SMSBroadcastReceiver

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startSMSRetriever()
broadcastReceiver = SMSBroadcastReceiver(this)
}

override fun onResume() {
super.onResume()
registerReceiver(broadcastReceiver, IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION))
}

override fun onPause() {
unregisterReceiver(broadcastReceiver)
super.onPause()
}

private fun startSMSRetriever() {
val client = SmsRetriever.getClient(this)
val task = client.startSmsRetriever()
task.addOnSuccessListener { aVoid ->
// do something
}.addOnFailureListener { e ->
// do something
Log.e(TAG, e.toString())
}.addOnCanceledListener {
// do something
}.addOnCompleteListener {
// do something
}
}
}

That’s it. We don’t need any SMS or CALL_LOG related permission then we can have an auto-populate OTP feature. Enjoy it :)

--

--