Auto-detect In-coming SMS with Google’s User Consent API.

Google’s User Consent API provides developers with a simple way to retrieve the contents of a text message, with the permission of the user. Users can give consent to apps, allowing them to read the content of a single text message rather than all the messages in their inbox. This is a nice middle ground between developers including dangerous permission their apps (android.permission.READ_SMS) and users having control of what messages an app can read. 

A common use case for using this API is to verify a user’s identity through a one-time password. This post walks you through the implementation.

App Flow.

  1. Retrieve the user’s number (either through a hint selector, or manual input).
  2. Send the user’s phone number to your remote server.
  3. Start listening for incoming messages.
  4. Parse the SMS content for the OTP when the user grants consent to read delivered SMS.

We start by adding the play services auth dependencies.

// play services auth component
implementation 'com.google.android.gms:play-services-auth:18.0.0'
implementation 'com.google.android.gms:play-services-auth-api-phone:17.4.0'

When the activity is launched, we attempt to retrieve the user’s phone number using the hint selector.

val hintRequest = HintRequest.Builder()
            .setPhoneNumberIdentifierSupported(true)
            .build()
        val credentialsClient = Credentials.getClient(this)
        val intent = credentialsClient.getHintPickerIntent(hintRequest)
        try {
            startIntentSenderForResult(
                intent.intentSender,
                CREDENTIAL_PICKER_REQUEST,
                null, 0, 0, 0
            )
        } catch (e: IntentSender.SendIntentException) {
            Log.e("MainActivity", "Could not start intent: $e")
        }

If the hint selector doesn’t display a valid user number to select, we manually input the phone number. We then initialize the SmsRetriever and attach an addOnCompleteListener to the task, so we can register a BroadcastReceiver to listen for the next incoming SMS.

val task = SmsRetriever.getClient(this).startSmsUserConsent(null)
task.addOnCompleteListener {
    task?.exception?.let {
        // handle possible exception
        Log.e(TAG, "Task Exception: $it")
        return@addOnCompleteListener
    }

    if (task.isSuccessful) {
        val intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
        registerReceiver(
            smsBroadcastReceiver, 
            intentFilter, 
            SmsRetriever.SEND_PERMISSION, 
            null
        )
    }
}

Note: Passing null to the startSmsUserConsent method is used if the phone number of the SMS sender is not known. Doing this would trigger the broadcast only if the sender of the SMS is not a saved contact on the device. If the phone number belonging to the sender of the incoming SMS is known, you can pass it as a parameter to the startSmsUserConsent method. This would still trigger the message broadcast even if the number is a saved contact.

Set up your BroadcastReceiver to handle the incoming SMS.

class SmsBroadcastReceiver : BroadcastReceiver() {
    companion object {
        const val TAG = "SmsBroadcastReceiver"
        const val REQUEST_SMS_CONSENT = 1001
    }
    override fun onReceive(context: Context?, intent: Intent?) {
        if (SmsRetriever.SMS_RETRIEVED_ACTION == intent?.action) {
            val extras = intent.extras
            val smsRetrieverStatus = extras?.get(SmsRetriever.EXTRA_STATUS) as Status

            when (smsRetrieverStatus.statusCode) {
                CommonStatusCodes.SUCCESS -> {
                    val consentIntent = 
                        extras.getParcelable<Intent>(SmsRetriever.EXTRA_CONSENT_INTENT)

                    try {
                        // Start Activity to show content dialog to user. 
                        // Activity must be started
                        // within 5 minutes, otherwise you'll receive another TIMEOUT 
                        // intent
                        context?.let { 
                          (it as AppCompatActivity)
                            .startActivityForResult(consentIntent!!, REQUEST_SMS_CONSENT) 
                        }

                    } catch (e: ActivityNotFoundException) {
                        Log.e(TAG, "No Activity Found: $e")
                    }
                }

                CommonStatusCodes.TIMEOUT -> {
                    // handle timeout
                }
             }
        }
    }
}

And in your Activity’s onActivityResult you can parse the message contents to retrieve the OTP.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            REQUEST_SMS_CONSENT -> {
                if (resultCode == Activity.RESULT_OK) {
                    data?.let {
                        val message = it.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE)
                        parseMessageForOtp(message)
                    }
                }
            }
        }
    }

Here’s a link to the project repo.


Leave a Reply

Your email address will not be published. Required fields are marked *