Belajar membuat One Time Password (OTP) di Android (bagian 2)
Cara membaca OTP lewat SMS di Android
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.
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.