最近收到团队内部的一个需求,使用一台测试机来做短信转发,即写一个 App 监听短信,当收到的短信内容符合我们设定的规则时,通过接口传送给我们后端。
这个需求实际上并不难,有两个思路。
拦截短信广播
当手机收到短信时,会向系统发送一个广播,所以我们可以通过监听广播来获取短信内容。
public class SmsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (!action.equals("android.provider.Telephony.SMS_RECEIVED")) {
return;
}
Bundle bundle = intent.getExtras();
if (bundle == null) {
return;
}
Object[] pdus = (Object[]) bundle.get("pdus");
if (pdus == null) {
return;
}
SmsMessage[] messages = new SmsMessage[pdus.length];
StringBuilder sender = new StringBuilder();
StringBuffer content = new StringBuffer();
StringBuffer time = new StringBuffer();
for (int i = 0; i < pdus.length; i++) {
sender.append(messages[i].getOriginatingAddress());
content.append(messages[i].getMessageBody());
time.append(messages[i].getTimestampMillis());
}
...
}
}
接收短信时发送的是一个有序广播,所以我们可以在注册时提高其优先级,避免被其他软件拦截:
<manifest ...>
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<application ...>
<receiver
android:name=".SmsReceiver"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="1000">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
</application>
</manifest>
同时不要忘了动态申请 RECEIVE_SMS
权限。
这样就可以实时监听到短信的接收了,上面的代码我获取了发件人、短信内容以及时间戳,其他参数你也可以自行获取。
监听短信媒体库变化
除了监听系统广播,我们还可以监听短信媒体库的变化,短信一般都是存储在本地数据库中,我们能够像操作 SQLite 一样读取它。
public class SmsObserver extends ContentObserver {
public SmsObserver(Handler handler, Context context) {
super(handler);
interceptSms(context);
}
private void interceptSms(Context context) {
Cursor cursor = context.getContentResolver().query(Uri.parse("content://sms/inbox"), null, null, null, "date DESC");
if (cursor.moveToFirst()) {
String sender = cursor.getString(cursor.getColumnIndex("address"));
String content = cursor.getString(cursor.getColumnIndex("body"));
long date = cursor.getLong(cursor.getColumnIndex("date"));
int type = cursor.getInt(cursor.getColumnIndex("type")); // 1:接收,2:发送
if (type == 1) {
...
}
}
cursor.close();
}
}
读取短信媒体库需要 READ_SMS
权限:
<manifest ...>
<uses-permission android:name="android.permission.READ_SMS" />
...
</manifest>
动态申请成功后,在代码中注册:
SmsObserver smsObserver = new SmsObserver(new Handler(), this);
getContentResolver().registerContentObserver(Uri.parse("content://sms"), true, smsObserver);
这里给定的 Uri
可以查询不同的短信协议:
- 收件箱:
content://sms/inbox
- 发件箱:
content://sms/outbox
- 已发送:
content://sms/sent
- 草稿:
content://sms/draft
- 发送失败:
content://sms/failed
- 待发送:
content://sms/queued
通过 Cursor
查询并按照时间戳排序即可拿到最新的短信。
测试
测试也比较简单,两台手机就可以完成测试。
假如你没有两台手机,又或者心疼话费,那么也可以通过『Android Studio』自带的模拟器进行测试。
在实际生产环境应当在真机上进行测试,因为国产 ROM 魔改出于隐私考虑有可能会对短信做处理。
比较
通过广播拦截的方式,假如有其他应用的 BroadcastReceiver
高于你设定的优先级,那么你可能无法拦截到此短信。
而监听短信媒体库的方式,由于状态繁多,会导致 onChange()
方法多次触发,造成不必要的计算,即使是同一条短信,在接收时触发回调,在阅读时也会触发,应该是短信状态变更导致。
使用国产 ROM 测试发现,两种方法监听验证码类短信都无法 100% 成功,通过广播拦截方式,会接收不到该短信的广播,而使用监听短信媒体库的方式,会触发媒体库变动的接口,但你依然查不到验证码的短信。
由于模拟器没有实体 SIM 卡无法接收真实的短信,且国外对于短信验证码的普及率不高,所以无法测试也没有测试的必要。
如有条件可以使用国外手机如 Google Pixel 系列进行测试。
因此,暂时找不到能够拦截短信验证码的方法,但这也的确符合常理,因为在国内互联网环境下,获取到验证码就可以对其对应的账号进行各种操作,比如重置或盗刷等等,系统对于此类短信的屏蔽是完全合理的。
综上,对于非验证码类短信可以正常拦截,根据自己的需求选择不同的方法即可,对于验证码类短信,就别动坏心思了。