一年經驗的全棧程序員,目前頭發健在,但不知道能撐多久。
該項目已成功部署并穩定運行于企業生產環境,如需個性化定制方案,歡迎聯系作者進行深度合作。
文章目錄
前言
一、頁面設計
1.頁面顯示
?2.代碼實現
?二、具體代碼實現
1.添加網絡權限和短信權限
2.實現短信監聽(BroadcastReceiver)
3.AndroidManifest.xml?中注冊廣播接收器
4. 封裝網絡請求(HttpURLConnection)?
?三、MainActivity主程序編寫
1. 權限管理模塊
2. 短信接收與處理模塊?
?3. 數據存儲與展示模塊
4. 用戶配置管理模塊
5. 定時清理模塊(可選)?
?總結
🙌?求點贊、收藏、關注!?
前言
由于公司業務需求需要監控大批手機號的驗證碼所以有了這個項目,在 Android 應用開發中,短信監控通常用于合規場景,如企業設備管理、金融風控(驗證碼自動填充)或家長監護。不同于直接讀取短信數據庫(ContentProvider
),使用?BroadcastReceiver
?監聽短信廣播(android.provider.Telephony.SMS_RECEIVED
)是一種更輕量、實時性更強的方案。
本文將介紹如何通過?BroadcastReceiver
?捕獲短信,并使用?原生?HttpURLConnection
(而非第三方庫)將數據安全上傳至服務器,涵蓋以下關鍵點:
-
短信監聽機制:注冊廣播接收器,過濾有效短信(如特定發送方或驗證碼)。
-
網絡請求實現:手動封裝?
HttpURLConnection
,支持?POST/GET
?請求。 -
安全與合規性:
-
動態申請?
RECEIVE_SMS
?權限,確保用戶知情同意。 -
避免存儲敏感信息,僅傳輸必要數據。
-
注意:未經用戶授權的短信監控屬于違法行為,本文僅限技術探討,請確保應用符合 Google Play 政策及《個人信息保護法》。
一、頁面設計
由于沒有做統一的日志管理但是也需要查看短信是否有監聽到使用頁面需要顯示監聽的手機號和內容。
1.頁面顯示
?2.代碼實現
<?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=".MainActivity"><!-- 應用名稱標題 --><TextViewandroid:id="@+id/appNameTextView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:padding="16dp"android:text="短信接收器"android:textSize="20sp"android:textStyle="bold"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent" /><!-- 卡1標題 --><Buttonandroid:id="@+id/baoc"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="16dp"android:onClick="save"android:text="保存"app:layout_constraintBottom_toBottomOf="@+id/appNameTextView"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="@+id/appNameTextView" /><TextViewandroid:id="@+id/card1TitleTextView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginTop="16dp"android:text="卡1:"android:textSize="16sp"android:textStyle="bold"app:layout_constraintBottom_toBottomOf="@+id/editTextPhone1"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintTop_toBottomOf="@+id/appNameTextView" /><!-- 卡1輸入框 --><EditTextandroid:id="@+id/editTextPhone1"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="8dp"android:layout_marginEnd="8dp"android:hint="輸入卡1號碼"android:textSize="16sp"app:layout_constraintLeft_toRightOf="@+id/card1TitleTextView"app:layout_constraintRight_toLeftOf="@+id/switch1"app:layout_constraintTop_toTopOf="@+id/card1TitleTextView" /><!-- 卡1開關 --><Switchandroid:id="@+id/switch1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="16dp"app:layout_constraintBottom_toBottomOf="@+id/editTextPhone1"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="@+id/editTextPhone1" /><!-- 卡2標題 --><TextViewandroid:id="@+id/card2TitleTextView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginTop="16dp"android:text="卡2:"android:textSize="16sp"android:textStyle="bold"app:layout_constraintBottom_toBottomOf="@+id/editTextPhone2"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintTop_toBottomOf="@+id/editTextPhone1" /><!-- 卡2輸入框 --><EditTextandroid:id="@+id/editTextPhone2"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="8dp"android:layout_marginEnd="8dp"android:hint="輸入卡2號碼"android:textSize="16sp"app:layout_constraintLeft_toRightOf="@+id/card2TitleTextView"app:layout_constraintRight_toLeftOf="@+id/switch2"app:layout_constraintTop_toTopOf="@+id/card2TitleTextView" /><!-- 卡2開關 --><Switchandroid:id="@+id/switch2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="16dp"app:layout_constraintBottom_toBottomOf="@+id/editTextPhone2"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="@+id/editTextPhone2" /><!-- 短信內容顯示框 --><TextViewandroid:id="@+id/smsTextView"android:layout_width="0dp"android:layout_height="300dp"android:layout_marginTop="8dp"android:background="#000000"android:padding="8dp"android:text="監控短信中"android:textColor="#FFFFFF"android:scrollbars="vertical"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toBottomOf="@+id/editTextPhone2" /></androidx.constraintlayout.widget.ConstraintLayout>
為什么這里需要設置卡號顯示起除是因為安卓開發系統的匹配度問題有些手機系統是不能自動識別手機號的所以顯示這個卡號是看看有沒有自動識別,如果沒有需要手動輸入并且保存,以為后面是根據具體卡槽id識別是哪個手機號接收到的驗證碼。
?二、具體代碼實現
1.添加網絡權限和短信權限
在AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
2.實現短信監聽(BroadcastReceiver)
新建一個SMSReceiver.class服務監聽短信
public class SMSReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {// 1. 從廣播意圖中提取短信數據Bundle bundle = intent.getExtras();if (bundle != null) {// 2. 獲取短信PDU數組和SIM卡訂閱IDObject[] pdus = (Object[]) bundle.get("pdus");int subscriptionId = bundle.getInt("subscription", -1);if (pdus != null) {for (Object pdu : pdus) {// 3. 解析短信內容(發送方、正文)SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) pdu);String messageBody = smsMessage.getMessageBody();String sender = smsMessage.getDisplayOriginatingAddress();// 4. 獲取SIM卡槽位信息(雙卡場景)String slotInfo = getSlotInfo(context, subscriptionId);// 5. 發送自定義廣播傳遞短信數據Intent smsIntent = new Intent("smsReceived");smsIntent.putExtra("message", messageBody);smsIntent.putExtra("sender", sender);smsIntent.putExtra("slotInfo", slotInfo);context.sendBroadcast(smsIntent);}}}}/*** 獲取SIM卡槽位信息(兼容雙卡)* @param subscriptionId SIM卡訂閱ID* @return 如 "Slot 1" 或 "Unknown Slot"*/private String getSlotInfo(Context context, int subscriptionId) {// 實現邏輯:通過SubscriptionManager查詢對應SIM卡槽// ...}/*** 獲取接收方手機號(需權限)* @note 因權限問題已注釋,實際使用需動態申請READ_PHONE_STATE權限*/@RequiresApi(api = Build.VERSION_CODES.N)private String getReceiverPhoneNumber(Context context, int subscriptionId) {// 實現邏輯:通過TelephonyManager獲取本機號碼// ...}
}
3.
AndroidManifest.xml
?中注冊廣播接收器
<receiver android:name=".SMSReceiver"android:exported="true"android:enabled="true"><intent-filter><action android:name="android.provider.Telephony.SMS_RECEIVED"/></intent-filter></receiver>
4. 封裝網絡請求(HttpURLConnection)?
public class MyRequest {public String post(String url1, String data) {try {URL url = new URL(url1);HttpURLConnection Connection = (HttpURLConnection) url.openConnection();//創建連接Connection.setRequestMethod("POST");Connection.setConnectTimeout(3000);Connection.setReadTimeout(3000);Connection.setDoInput(true);Connection.setDoOutput(true);Connection.setUseCaches(false);Connection.connect();DataOutputStream dos = new DataOutputStream(Connection.getOutputStream());String title = data;//這里是POST請求需要的參數字符串類型,例如"id=1&data=2"dos.write(title.getBytes());dos.flush();dos.close();//寫完記得關閉int responseCode = Connection.getResponseCode();if (responseCode == Connection.HTTP_OK) {//判斷請求是否成功InputStream inputStream = Connection.getInputStream();ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();byte[] bytes = new byte[1024];int length = 0;while ((length = inputStream.read(bytes)) != -1) {arrayOutputStream.write(bytes, 0, length);arrayOutputStream.flush();}//讀取響應體的內容String s = arrayOutputStream.toString();return s;//返回請求到的內容,字符串形式} else {return "-1";//如果請求失敗返回-1}} catch (Exception e) {return "-1";//出現異常也返回-1}}public String get(String url1) {try {URL url = new URL(url1);HttpURLConnection Connection = (HttpURLConnection) url.openConnection();Connection.setRequestMethod("GET");Connection.setConnectTimeout(3000);Connection.setReadTimeout(3000);int responseCode = Connection.getResponseCode();if (responseCode == Connection.HTTP_OK) {InputStream inputStream = Connection.getInputStream();ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();byte[] bytes = new byte[1024];int length = 0;while ((length = inputStream.read(bytes)) != -1) {arrayOutputStream.write(bytes, 0, length);arrayOutputStream.flush();//強制釋放緩沖區}String s = arrayOutputStream.toString();return s;} else {return "-1";}} catch (Exception e) {return "-1"+e;}}}
?三、MainActivity主程序編寫
1. 權限管理模塊
// 定義所需權限
private static final int PERMISSION_REQUEST_CODE = 1;
String[] permissions = {Manifest.permission.READ_SMS, Manifest.permission.RECEIVE_SMS,Manifest.permission.READ_PHONE_STATE,Manifest.permission.READ_PHONE_NUMBERS
};// 檢查并請求權限
if (未全部授權) {ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);
} else {registerSmsReceiver(); // 注冊廣播接收器
}// 權限請求結果回調
@Override
public void onRequestPermissionsResult(...) {if (權限通過) {registerSmsReceiver();} else {showToast("短信監控功能不可用");}
}
2. 短信接收與處理模塊?
// 自定義廣播接收器
private BroadcastReceiver smsReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 解析短信數據String message = intent.getStringExtra("message");String sender = intent.getStringExtra("sender");String slotInfo = intent.getStringExtra("slotInfo");// 根據SIM卡槽匹配接收號碼String receiverNumber = slotInfo.contains("1") ? editTextPhone1.getText().toString() : editTextPhone2.getText().toString();// 過濾重復消息if (pdMessages(message)) {// 啟動網絡請求線程new Thread(() -> {String url = "服務器接口;String response = new MyRequest().get(url);handleResponse(response, receiverNumber, sender, message);}).start();}}
};// 消息去重檢查
private boolean pdMessages(String mess) {Set<String> savedMessages = sp.getStringSet("messages", new HashSet<>());if (savedMessages.contains(mess)) {return false; // 重復消息}savedMessages.add(mess);sp.edit().putStringSet("messages", savedMessages).apply();return true;
}
?3. 數據存儲與展示模塊
// 存儲消息記錄(包含狀態和時間戳)
private void storeMessageStatus(String receiver, String sender, String message, String status) {String key = "message_" + System.currentTimeMillis();String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());String record = String.format("接收號: %s\n發送方: %s\n時間: %s\n內容: %s\n狀態: %s\n",receiver, sender, time, message, status);sp.edit().putString(key + "_data", record).putLong(key + "_time", System.currentTimeMillis()).apply();
}// 顯示歷史消息
private void displayStoredMessages() {StringBuilder sb = new StringBuilder();for (Map.Entry<String, ?> entry : sp.getAll().entrySet()) {if (entry.getKey().endsWith("_data")) {sb.append(entry.getValue()).append("\n");}}smsTextView.setText(sb.length() > 0 ? sb.toString() : "無記錄");
}
4. 用戶配置管理模塊
// 保存用戶設置的手機號
public void save(View view) {sp.edit().putString("editTextPhone1", editTextPhone1.getText().toString()).putString("editTextPhone2", editTextPhone2.getText().toString()).apply();showToast("保存成功");
}// 初始化時加載配置
private void loadConfig() {editTextPhone1.setText(sp.getString("editTextPhone1", ""));editTextPhone2.setText(sp.getString("editTextPhone2", ""));
}
5. 定時清理模塊(可選)?
// 設置每天中午12點清理舊數據
private void setDailyCleanupAlarm() {AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);Intent intent = new Intent(this, CleanupReceiver.class);PendingIntent pendingIntent = PendingIntent.getBroadcast(...);Calendar calendar = Calendar.getInstance();calendar.set(Calendar.HOUR_OF_DAY, 12); // 12:00 PMif (已過當前時間) calendar.add(Calendar.DAY_OF_YEAR, 1);alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
}
?總結
-
實現了雙卡短信監控:通過
BroadcastReceiver
捕獲短信,并根據SIM卡槽自動匹配預設的手機號,支持雙卡場景下的短信分類處理。 -
數據安全與合規性:動態申請權限確保用戶知情權,使用
SharedPreferences
存儲消息記錄,避免敏感信息泄露,符合隱私保護要求。 -
網絡上傳與狀態反饋:通過
HttpURLConnection
將短信內容上傳至服務器,并實時顯示發送狀態(成功/失敗),數據持久化便于追溯。 -
可擴展性強:模塊化設計(權限管理、消息處理、數據存儲)便于后續擴展,如增加加密傳輸或對接其他API。
🙌?求點贊、收藏、關注!?
如果這篇文章對你有幫助,不妨:
👍?點個贊?→ 讓更多人看到這篇干貨!
??收藏一下?→ 方便以后隨時查閱!
🔔?加關注?→ 獲取更多?前端/后端/全棧技術深度解析!
你的支持,是我持續創作的最大動力!?🚀