Gửi tin nhắn SMS tự động trên Android có lẽ là yêu cầu khá cơ bản mà nhiều bạn khi làm Android gặp phải. Code Tu Tam hôm nay sẽ hướng dẫn các bạn xây dựng tool gửi tin nhắn SMS trên Android. Code gửi tin nhắn sms android sẽ được triển khai với Kotlin. CodeTuTam cũng chỉ là dân ngoại đạo và triển khai tool này theo yêu cầu.
Chính vì lẽ vậy, các kiến thức chia sẻ trong bài viết này có thể không phải cách chuẩn tắc, cũng như tối ưu. Tuy nhiên nội dung trong bài được viết dựa trên kiến thức kinh nghiệm có được trong quá trình triển khai của CodeTuTam
Nếu các bạn chưa biết nhiều về Kotlin có thể tham khảo thêm các bài viết
Hướng dẫn tạo dự án tool gửi SMS Android trên Android studio
Tạo project mới trong Android Studio
Để khởi tạo một dự án Android mới, bạn có nhiều cách khác nhau. Để đơn giản có thể sử dụng Android Studio.
Các bạn có thể chọn luôn Basic Activity. Với lựa chọn khi xây dựng tool gửi tin nhắn trong Android thì bàn sẽ có sẵ các màn hình cơ bản để triển khai thay vì phải khởi tạo từ đầu.
Khi khởi tạo dự án với Android studio bạn sẽ thấy các lựa chọn như trên hình. Trong đó
Name: Tên dự án của bạn
Package name: là tên package của dự án, cũng giống như namespace trong PHP vậy
Save Location: Thư mục chứa mã nguồn chương trình
Language: Ngôn ngữ lập trình, trong ví dụ này mình sẽ sử dụng Kotlin. Các bạn có thể sử dụng Java như là một ngôn ngữ chính cho triển khai app Android.
Minimum SDK: là phiên bản Android tối thiểu có thể chạy ứng dụng này.
Cấu trúc thư mục của một project Android
Sau khi khởi tạo dự án chúng ta sẽ thấy một màn hình như sau
Trong đó, cột bên trái là khu vực hiển thị cấu trúc thư mục dự án Android chúng ta đang làm, cột bên phải là nội dung file code đang xem. Giao diện này thì cũng quá quen với các bạn lập trình rồi có lẽ cũng không cần phải nói nhiều nữa.
Theo sự tìm hiểu của mình thì cấu trúc dự án Android sẽ bao gồm các phần như sau:
File AndroidManifest.xml: Trong file này sẽ định nghĩa các cấu hình, quyền truy cập, Service, Activity… của ứng dụng chúng ta đang triển khai
Thư mục Java: là thư mục chứa mã nguồn code chính của dự án Android. Trong này có thể chứa code java và kotlin
Thư mục Res: Res viết tắt của Resouces, là thư mục chứa tài nguyên của tool gửi SMS chúng ta đang triển khai. Trong đó 1 số thư mục như drawable là chứa hình ảnh, layout là chứa các file layout giao diện của ứng dụng… Tệp tin chúng ta cần quan tâm sẽ là activity_main.xml và fragment_first.xml.
Gradle Scripts: Phần này chắc là khai báo các thư viện cấu hình, cũng giống như kiểu package.json hay compose.json thì phải (theo mình hiểu là vậy)
Xây dựng giao diện của ứng dụng gửi SMS với Android
Để cho tiện lợi, chúng ta sẽ sử dụng các giao diện mà template này cung cấp
Cập nhật file cấu hình giao diện ứng dụng gửi tin nhắn
Đầu tiên chúng ta sẽ cần xem nội dung file activity_main.xml
Trong file này có một số view AppBar gì đó, mình cũng không quan tâm lắm, tuy nhiên có 1 button. Button này mình không sử dụng và sẽ xóa đi để tránh rối mắt
Khi đó nội dung file activity_main.xml sẽ có nội dung như sau:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout 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"> <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/Theme.Testapp.AppBarOverlay"> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/Theme.Testapp.PopupOverlay" /> </com.google.android.material.appbar.AppBarLayout> <include layout="@layout/content_main" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>
Trong file activity_main.xml chúng ta có thấy 1 lệnh là include layout/content_main. Phần này chính là nội dung hiển thị các Fragment trong Android.
Đến phần này nhiều bạn có thể thắc mắc Fragment là gì hay Activity là gì?
Hiểu đơn giản thì, Activity là một màn hình ứng dụng trong Android. Mỗi 1 màn hình thực thi có thể tương ứng với 1 Activity. Ví dụ như Home Activity, Contact Avitity – nó cũng giống như Page của chúng ta khi làm website vậy.
Vậy còn Fragment là gì? Trong 1 Activity thì có thể có nhiều Fragment. Fragment này hiểu nôm na cũng giống như các slider ảnh trong lập trình web vậy. Mỗi Fragment có thể đảm nhiệm các nhiệm vu, nội dung khác nhau. Các Fragment trong 1 Activity có thể ẩn hiện tùy ý để đạt được mục đích
Cả Fragment hay Activity đều sử dụng layout được cấu hình trong file XML.
Tiếp đến chúng ta xem xét file fragment_first.xml, đây chính là file cấu hình giao diện của Fragment First
Trong file này ta thấy có 1 TextView và Button. Textview có lẽ chúng ta cũng không cần thiết lắm nên sẽ loại bỏ đi.
<?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" tools:context=".FirstFragment"> <Button android:id="@+id/button_first" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Start" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
Về cơ bản giao diện của ứng dụng gửi SMS trên Android như vậy là ổn rồi.
Khởi tạo một số file cần thiết trong việc xây dựng ứng dụng gửi tin nhắn
Khởi tạo file SmsService
Để tìm hiểu Service trong Android các bạn có thể Google để hiểu thêm. Tuy nhiên theo góc nhìn non nớt của mình thì Service là một dịch vụ chạy ngầm, ví dụ như Service Play music chả hạn. Do vậy để ứng dụng của chúng ta có thể chạy ngầm, chúng ta cần khai báo 1 Service. Nội dung code cho Service này chúng ta sẽ triển khai trong phần tiếp theo.
Nội dung file SmsService.kt như sau
package com.xs.testapp import android.app.Service import android.content.Intent import android.os.IBinder class SmsService : Service() { override fun onBind(p0: Intent?): IBinder? { TODO("Not yet implemented") } }
Cập nhật nội dung file AndroidManifest.xml
Trong file cấu hình của ứng dụng, ta sẽ khai báo Service, xin 1 số quyền cần sử dụng cho ứng dụng
Nội dung file AndroidManifest.xml sau khi cập nhật như sau
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.xs.testapp"> <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.SEND_SMS" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Testapp" tools:targetApi="31"> <service android:name=".SmsService" android:enabled="true" android:exported="true" > </service> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.Testapp.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Trong dó ứng dụng gửi tin nhắn của chúng ta xin một số quyền như sau
READ_PHONE_NUMBERS, READ_PHONE_STATE: để đọc thông tin về điện thoại, lấy thông tin về sim, có ích nếu như điện thoại bạn sử dụng 2 sim
SEND_SMS: Quyền cho phép ứng dụng được phép gửi tin nhắn
INTERNET: Quyền cho phép ứng dụng được truy cập internet. Trên thực tế các cấu hình ứng dụng cần phải lấy qua API về thay vì cấu hình ở trực tiếp ứng dụng.
FOREGROUND_SERVICE: Quyền cho phép chạy ứng dụng ở dạng ngầm
Cập nhật nội dung file MainActivity.kt
File MainActivity.kt chính là file code của màn hình chính ứng dụng. Các hạn có thể hiểu rằng hàm onCreate trong file này sẽ được gọi đến khi ứng dụng được khởi chạy, nói cách khác, khi MainActivity này được khởi tạo
Trong hàm này chúng ta thực hiện xin 1 số quyền thực thi ứng dụng
Mã nguồn file MainActivity.kt sẽ như sau
package com.xs.testapp import android.Manifest import android.content.pm.PackageManager import android.os.Bundle import com.google.android.material.snackbar.Snackbar import androidx.appcompat.app.AppCompatActivity import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.navigateUp import androidx.navigation.ui.setupActionBarWithNavController import android.view.Menu import android.view.MenuItem import androidx.core.app.ActivityCompat import com.xs.testapp.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var binding: ActivityMainBinding private val PERMISSION_REQUEST_CODE = 100 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) val navController = findNavController(R.id.nav_host_fragment_content_main) appBarConfiguration = AppBarConfiguration(navController.graph) setupActionBarWithNavController(navController, appBarConfiguration) if(!checkPermission()){ ActivityCompat.requestPermissions(this, arrayOf( Manifest.permission.READ_SMS, Manifest.permission.SEND_SMS, Manifest.permission.READ_PHONE_NUMBERS, Manifest.permission.READ_PHONE_STATE ),PERMISSION_REQUEST_CODE) } } private fun checkPermission():Boolean{ return (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_SMS ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_NUMBERS ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS ) != PackageManager.PERMISSION_GRANTED) } override fun onCreateOptionsMenu(menu: Menu): Boolean { // Inflate the menu; this adds items to the action bar if it is present. menuInflater.inflate(R.menu.menu_main, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. return when (item.itemId) { R.id.action_settings -> true else -> super.onOptionsItemSelected(item) } } override fun onSupportNavigateUp(): Boolean { val navController = findNavController(R.id.nav_host_fragment_content_main) return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() } }
Khởi tạo tệp tin Customer.kt
File Customer.kt có thể coi là DTO dùng để chứa thông tin về 1 đối tượng mà chúng ta muốn gửi SMS tới.
Nội dung file Customer.kt như sau
package com.xs.testapp class Customer( var id:String, var phone:String, var content:String, var sim:Int = 0 ) { override fun toString(): String { return "$id - $phone - $content - $sim"; } }
Trong class Customer chúng ta có các thuộc tính: id, phone, content, sim
Chúng ta sẽ biết được nội dung được gửi tới số điện thoại nào và bởi sim nào (sim 1 hay sim 2)
Khởi tạo class SmsSender
Class này đảm nhiệm việc gửi SMS tới một customer đã xác định
Nội dung file SmsSender.kt như sau
package com.xs.testapp import android.Manifest import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.telephony.SmsManager import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.widget.Toast import androidx.core.app.ActivityCompat class SmsSender (var context:Context) { companion object{ val SENT = "SMS_SENT" val DELIVERED = "SMS_DELIVERED" } public fun send(customer:Customer){ val sentIntent:Intent = Intent(SENT); sentIntent.putExtra("smsNumber", customer.phone); sentIntent.putExtra("customerId", customer.id); var sentPI = PendingIntent.getBroadcast(this.context, 0, sentIntent, PendingIntent.FLAG_CANCEL_CURRENT) val deliveredIntent:Intent = Intent(DELIVERED); deliveredIntent.putExtra("smsNumber", customer.phone); deliveredIntent.putExtra("customerId", customer.id); var deliveredPI= PendingIntent.getBroadcast(this.context, 0,deliveredIntent, PendingIntent.FLAG_CANCEL_CURRENT) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { val localSubscriptionManager = SubscriptionManager.from(this.context) val checkPermission = ActivityCompat.checkSelfPermission(this.context,Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED; if (checkPermission){ Toast.makeText(this.context,"Ứng dụng chưa cấp quyền",Toast.LENGTH_LONG).show(); return; } if (localSubscriptionManager.activeSubscriptionInfoCount > 1) { val localList: List<*> = localSubscriptionManager.activeSubscriptionInfoList val simIndex = customer.sim; val simInfo = localList[simIndex] as SubscriptionInfo SmsManager.getSmsManagerForSubscriptionId(simInfo.subscriptionId) .sendTextMessage(customer.phone, null, customer.content, sentPI, deliveredPI) } } else { SmsManager.getDefault() .sendTextMessage(customer.phone, null, customer.content, sentPI, deliveredPI) } } }
Trong SmsSender có 1 hàm chính là hàm send. Hàm send nhận tham số đầu vào là 1 đối tượng customer. Trong hàm send sẽ xử lý để gửi lần lượt qua sim 1 và sim2 nếu điện thoại chạy ứng dụng này có hệ điều hành lớn hơn phiên bản Lollipop.
Trong này cúng ta cũng tìm hiểu về 1 số khái niệm, pendingIntent, SmsManager…
Khởi tạo tệp tin/class SmsService
Trong class SmsService có nhiệm vụ đăng ký BroastcastReceiver và khởi tạo một tiến trình
BroadcastReceiver là gì? BroadcastReceiver các bạn có thể hiểu rằng nó dùng để lắng nghe các thông báo được ném ra từ ứng dụng hoặc hệ thống Android. Cụ thể trong trường hợp tool gửi SMS này là dùng để nhận các thông báo về việc gửi tin nhắn thành công/thất bại hay là tin nhắn được nhận thành công hay chưa
Trong class SmsService cũng khai báo 2 Broastcast cho việc nhận thông tin SMS này.
Ngoài ra còn khai báo 1 Thread được thiết lập vòng lặp while true để gửi tin nhắn liên tục
Tiếp đến, để duy trì trạng thái hoạt động trong cả chế độ nền, Service của chúng ta sử dụng thêm Notification. Các bạn có thể tham khảo đoạn code dưới đây.
package com.xs.testapp import android.R import android.app.* import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build import android.os.IBinder import android.telephony.SmsManager import android.widget.Toast import androidx.core.app.NotificationCompat class SmsService : Service() { val CHANNEL_ID = "ForegroundServiceChannel" val smsSender by lazy { SmsSender(this) }; class SmsLoop(val smsSender: SmsSender,val context: Context): Thread() { public override fun run() { while (true){ val customer = Customer("1","0912345678","Test",0); smsSender.send(customer) Thread.sleep(15000); } } } val smsLoop by lazy { SmsLoop(smsSender,this); } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) createNotificationChannel() val notificationIntent = Intent(this, MainActivity::class.java) val pendingIntent = PendingIntent.getActivity( this, 0, notificationIntent, 0 ) val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Sms Foreground Service") .setContentText("SMS Sender") .setSmallIcon(R.drawable.btn_star) .setContentIntent(pendingIntent) .build() startForeground(1, notification) this.registerSmsBroast(); smsLoop.start(); return START_NOT_STICKY } fun registerSmsBroast(){ val sendSMS: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(arg0: Context?, intent: Intent?) { var resultCodeString: String? = null; val smsNumber = intent?.getStringExtra("smsNumber")?:""; val customerId = intent?.getStringExtra("customerId")?:""; when (resultCode) { Activity.RESULT_OK -> { resultCodeString = "RESULT_OK"; } SmsManager.RESULT_ERROR_GENERIC_FAILURE -> { resultCodeString = "RESULT_ERROR_GENERIC_FAILURE"; } SmsManager.RESULT_ERROR_NO_SERVICE -> { resultCodeString = "RESULT_ERROR_NO_SERVICE"; } SmsManager.RESULT_ERROR_NULL_PDU -> { resultCodeString = "RESULT_ERROR_NULL_PDU"; } SmsManager.RESULT_ERROR_RADIO_OFF -> { resultCodeString = "RESULT_ERROR_RADIO_OFF"; } } println("sendSMS $resultCodeString-$smsNumber-$customerId"); } } val deliverSMS: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(arg0: Context?, intent: Intent?) { val smsNumber = intent?.getStringExtra("smsNumber")?:""; val customerId = intent?.getStringExtra("customerId")?:""; var resultCodeString:String? = null; when (resultCode) { Activity.RESULT_OK -> { resultCodeString = "RESULT_OK" } Activity.RESULT_CANCELED -> { resultCodeString = "RESULT_CANCELED" } } println(" deliverSMS $resultCodeString-$smsNumber-$customerId"); } } this.registerReceiver(sendSMS, IntentFilter(SmsSender.SENT)); this.registerReceiver(deliverSMS, IntentFilter(SmsSender.DELIVERED)); } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val serviceChannel = NotificationChannel( CHANNEL_ID, "SMS Foreground Service Channel", NotificationManager.IMPORTANCE_DEFAULT ) val manager = getSystemService( NotificationManager::class.java ) manager.createNotificationChannel(serviceChannel) } } override fun onDestroy() { super.onDestroy() smsLoop.interrupt() } override fun onBind(p0: Intent?): IBinder? { return null; } }
Trên thực tế khi mình làm, mình sẽ có cài đặt thêm thư viện Retrofit để gọi API lấy dữ liêu thông tin số điện thoại cần gửi, và sử dụng Moshi để decode các json nhận được
Để tìm hiểu về Retrofit các bạn có thể tham khảo thêm tại link sau: https://square.github.io/retrofit/
Như đã nói, trong mã nguồn sử dụng 1 Thread với while true và sleep 15 giây, nghĩa là 15 giây sẽ gửi tin nhắn 1 lần.
Tiếp đến là chúng ta chỉnh sửa file FirstFragment
Cập nhật nội dung file FirstFragment
Trong FirstFragment chúng ta sẽ khởi tạo SmsService bên trên, khi đó Service của chúng ta sẽ chạy ngầm và thực hiện gửi tin nhắn liên tục
Trong FirstFragment chúng ta khai báo sự kiện cho Button btnFirst của chúng ta, Trong sự kiện này sẽ gọi tới hàm startService
Nội dung class FirstFragment như sau
package com.xs.testapp import android.content.Intent import android.os.Build import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.navigation.fragment.findNavController import com.xs.testapp.databinding.FragmentFirstBinding /** * A simple [Fragment] subclass as the default destination in the navigation. */ class FirstFragment : Fragment() { private var _binding: FragmentFirstBinding? = null // This property is only valid between onCreateView and // onDestroyView. private val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentFirstBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.buttonFirst.setOnClickListener(startListener); } private val startListener = View.OnClickListener { view-> Toast.makeText(activity,"Start", Toast.LENGTH_LONG).show(); val intentService = Intent(activity, SmsService::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { activity?.startForegroundService(intentService); } else{ activity?.startService(intentService); } } private fun checkDualSim(){ } override fun onDestroyView() { super.onDestroyView() _binding = null } }
Đến bước này bạn đã hoàn thành 99% quá trình xây dựng tool spam sms trên Android.
Đương nhiên để sử dụng thực tế bạn cần cải thiện thêm 1 số phần giúp hoạt động dễ dàng và hiệu quả hơn.
Tuy vậy với hướng dẫn cơ bản này đã giúp các bạn có cái tổng quan về xây dựng một ứng dụng Android cơ bản với Kotlin, và xây dựng tool gửi tin nhắn SMS trên Android nói riêng.
Tổng kết bài viết viết tool spam sms Android
Qua bài viết này CodeTuTam đã chia sẻ kinh nghiệm xây dựng một ứng dụng Android bằng Kotlin từ con số 0. Trên thực tế, CodeTuTam có triển khai thêm 1 số đoạn mã để có thể lấy thông tin từ server về chạy. Các bạn có thể tham khảo đoạn mã CodeTuTam triển khai và mở rộng theo mục đích sử dụng của mình bạn nhé
Nhưng chú ý rằng, việc spam là điều không nên thực hiện. Gọi là tool spam sms, nhưng bạn chỉ nên dùng để gửi tin nhắn chăm sóc khách hàng, hoặc những người chủ động đăng ký nhận tin với bạn thôi. Nếu không thì rất nhiều vấn đề đợi bạn phía trước đấy :)))
Nếu bạn có bất kì câu hỏi hay thắc mắc nào có thể đặt câu hỏi trong phần bình luận. CodeTuTam sẽ hỗ trợ hết sức mình nếu chúng tôi biết. Cảm ơn bạn đã quan tâm và ủng hộ chúng tôi. Đừng quên like và share bài viết để ủng hộ chúng tôi bạn nhé