Belajar membuat One Time Password (OTP) di Android (bagian 2)

Cara membaca OTP lewat SMS di Android

Yudi Setiawan
Nusanet Developers
4 min readJan 20, 2019

--

Series

Pengantar

Setelah sebelumnya kita telah berhasil membuat server OTP yang berfungsi untuk mengirimkan SMS maka, pada bagian kedua ini kita akan membuat app di Android. Adapun output dari artikel bagian kedua ini adalah seperti berikut.

Output

Jadi, nanti app-nya kita buat untuk bisa membaca kode OTP dari SMS secara otomatis.

Atur Dependency

Silakan kita atur terlebih dahulu dependency-dependency yang kita perlukan seperti berikut.

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
implementation 'androidx.core:core-ktx:1.1.0-alpha03'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.retrofit2:retrofit-converters:2.3.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.google.code.gson:gson:2.8.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

Buat Layout Utama

Selanjutnya kita buat layout utama-nya seperti berikut.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:padding="16dp"
tools:context=".MainActivity">

<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/edit_text_1"
android:inputType="number"
android:maxLength="1"
android:maxLines="1"
android:gravity="center"
android:padding="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/edit_text_2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:ignore="Autofill,LabelFor"/>

<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/edit_text_2"
android:inputType="number"
android:maxLength="1"
android:maxLines="1"
android:gravity="center"
android:padding="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/edit_text_3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_text_1"
tools:ignore="Autofill,LabelFor"/>

<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/edit_text_3"
android:inputType="number"
android:maxLength="1"
android:maxLines="1"
android:gravity="center"
android:padding="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/edit_text_4"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_text_2"
tools:ignore="Autofill,LabelFor"/>

<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/edit_text_4"
android:inputType="number"
android:maxLength="1"
android:maxLines="1"
android:gravity="center"
android:padding="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_text_3"
tools:ignore="Autofill,LabelFor"/>

<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/edit_text_phone_number"
android:hint="Input phone number"
android:inputType="number"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:ignore="Autofill"/>

<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/button_send_activity_main"
android:text="Send"
app:layout_constraintTop_toBottomOf="@+id/edit_text_phone_number"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Buat API Service

Terlebih dahulu kita buat interface API Service pada projek kita seperti berikut.

interface Api {

@POST("otp/send")
fun sendOtp(@Query("phoneNumber", encoded = true) phoneNumber: String): Observable<ResponseBody>

@POST("otp/update")
fun updateOtp(@Query("codeOtp") codeOtp: String): Observable<ResponseBody>

}

Jadi, nantinya ada proses API yang akan kita lakukan pada projek kali ini yaitu, untuk mendapatkan kode OTP dari SMS maka, kita perlu kirim POST ke endpoint sendOtp . Selanjutnya, ketika kita mendapatkan kode tersebut dari SMS maka, kita kirim balik kode OTP tersebut ke endpoint updateOtp untuk memastikan bahwa kita benar-benar mendapatkan kode OTP yang valid.

Deklarasi API Service

Di file MainActivity kita perlu mendeklarasikan API Service yang kita buat tadi seperti berikut.

private fun initRetrofit() {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BASIC
val
client = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://server-otp.herokuapp.com/api/")
.client(client)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
api = retrofit.create(Api::class.java)
}

Permission

Untuk bisa menggunakan fitur SMS pada app dimana, pada kali ini kita memerlukan fitur menerima SMS dan membacanya. Lalu kita juga perlu permisi akses internet. Maka, kita perlu mendeklarasikan permisi-permisi tersebut di file AndroidManifest.xml.

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

Jangan lupa kita buat juga runtime permission-nya seperti berikut pada file MainActivity.

private fun checkRuntimePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(arrayOf(Manifest.permission.RECEIVE_SMS), 100)
}
}

Buat Receiver SMS

Untuk bisa mendapatkan listener ketika SMS masuk maka, kita bisa menggunakan BroadcastReceiver dimana, didalamnya kita akan membaca isis pesan SMS yang masuk.

class SmsReceiver : BroadcastReceiver() {

companion object {
private var smsListener: SmsListener? = null

fun
bindListener(smsListener: SmsListener) {
this.smsListener = smsListener
}
}

override fun onReceive(context: Context?, intent: Intent?) {
val extras = intent?.extras
val pdus = extras?.get("pdus") as Array<*>
for (item in pdus) {
val smsMessage: SmsMessage
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val format = extras.getString("format")
smsMessage = SmsMessage.createFromPdu(item as ByteArray, format)
} else {
smsMessage = SmsMessage.createFromPdu(item as ByteArray)
}
val message = smsMessage.messageBody
smsListener?.messageReceived(message)
}
}

}

Pada kode diatas bisa kita lihat bahwa ketika kita mendapatkan SMS masuk maka kita ada melakukan pemanggilan fungsi dari si interface SmsListener dimana, SmsListener ini kita deklarasikan pada file MainActivity. Adapun isi dari interface SmsListener seperti berikut.

interface SmsListener {

fun messageReceived(message: String)

}

Dan selanjutnya, kita tambahkan deklarasi si SmsListener pada MainActivity.

class MainActivity : AppCompatActivity(), SmsListener {

private fun bindSmsReceiver() {
SmsReceiver.bindListener(this)
}

@SuppressLint("CheckResult")
override fun messageReceived(message: String) {
// TODO: do something in here

}

}

Buat fitur kirim SMS

Selanjutnya, kita buat fitur untuk mengirim SMS-nya. Untuk membuatnya kita perlu menambahkan onClickListener pada button SEND dimana, didalamnya kita ada membuat proses seperti berikut.

  • Mengambil nilai phone number dari EditText
  • Menampilkan ProgressDialog
  • Mengirimkan POST ke endpoint sendOtp di API Service
button_send_activity_main.setOnClickListener { _ ->
var phoneNumber = edit_text_phone_number.text.toString()
if (phoneNumber.isBlank() || phoneNumber.isEmpty()) {
Toast.makeText(this, "Phone number is required", Toast.LENGTH_LONG)
.show()
return@setOnClickListener
}
phoneNumber = "+62${phoneNumber.substring(1)}"
showProgressDialog()
api.sendOtp(phoneNumber)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
hideProgressDialog()
Toast.makeText(this, "SMS has been sent", Toast.LENGTH_LONG)
.show()
},
{
it
.printStackTrace()
hideProgressDialog()
Toast.makeText(this, it.message, Toast.LENGTH_LONG)
.show()
}
)
}
private fun showProgressDialog() {
if (progressDialog == null) {
progressDialog = ProgressDialog(this@MainActivity)
progressDialog?.let {
it
.setCancelable(false)
it.setMessage("Please wait")
}
}
progressDialog!!.show()
}

private fun hideProgressDialog() {
progressDialog?.dismiss()
}

Buat fitur menerima SMS

Untuk membuat fitur menerima SMS kita perlu menambahkan sedikit proses logic didalam callback messageReceived dimana, proses logic-nya kira-kira seperti berikut.

  • Membaca isi SMS dan menampilkan-nya di EditText OTP
  • Kirim POST ke endpoint updateOtp di API Service
@SuppressLint("CheckResult")
override fun messageReceived(message: String) {
Log.d(javaClass.simpleName, "message: $message")
edit_text_1.setText(message[0].toString())
edit_text_2.setText(message[1].toString())
edit_text_3.setText(message[2].toString())
edit_text_4.setText(message[3].toString())
showProgressDialog()
api.updateOtp(message)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
hideProgressDialog()
val jsonObjectResponse = JSONObject(it.string())
if (jsonObjectResponse.getBoolean("success")) {
Toast.makeText(this, "OTP valid", Toast.LENGTH_LONG)
.show()
} else {
Toast.makeText(this, "OTP invalid", Toast.LENGTH_LONG)
.show()
}
},
{
it
.printStackTrace()
Toast.makeText(this, it.message, Toast.LENGTH_LONG)
.show()
}
)

}

Testing

Sekarang mari kita lakukan testing.

--

--