Android短信监听功能(解决onChange触发两次的问题)

[size=large][b]前言[/b][/size]
项目要做短信验证码自动填充的功能,基本上两种方法:ContentObserver监听短信数据库、广播。我个人比较喜欢监听数据库的方式,原因有以下几点:
1.广播方式只能读短信,不能写短信(比如更新短信状态为已读);而监听数据库则可以对短信做增删改查;
2.系统的短信广播是有序广播,如果有其他应用的优先级高于我们的应用,广播会先被其他应用截获,如果其他应用截获短信广播后将其消费掉并且不再继续发送,那我们的应用就收不到广播了;
3.广播方式需要注册receiver,动态注册还好,如果是静态注册,对于sdk类的产品来说,使用者有可能忘记在Manifest中注册;
基于以上原因,我的项目中索性就选择了监听数据库。监听数据库相对于广播稍微复杂点,但是更加灵活,功能更强大,所以还是建议使用监听数据库的方法,不过这里会将两种都说一下。

[size=large][b]方法一:监听短信数据库[/b][/size]
遇到的问题是大部分手机收到一条短信时会触发两次onChange()方法,很多人提出的解决方法是记录smsId,判断如果是旧的就不做处理,是新的就做该做的事。起初这么做了,后来发现有时候自动填充会先填旧的,立马又填新的,会闪一下,虽然只是偶尔出现,但还是觉得不爽,于是抽空又找别的方法,总算找出来了,也是受到一些网友的启发,下面就分享出来,原理我不多说了,代码注释里都讲得很清楚。

package com.jackie.bindmobile;

import android.app.Activity;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Monitor sms database
*
* @author Jackie
*
*/
public class SmsContent extends ContentObserver {

private static final String TAG = SmsContent.class.getSimpleName();
private static final String MARKER = "YOUR_KEYWORD";
private Cursor cursor = null;
private Activity mActivity;

public SmsContent(Handler handler, Activity activity) {
super(handler);
this.mActivity = activity;
}

/**
* This method is called when a content change occurs.
*


* Subclasses should override this method to handle content changes.
*


*
* @param selfChange True if this is a self-change notification.
*/
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
Log.d(TAG, "onChange(boolean selfChange). selfChange=" + selfChange);
onChange(selfChange, null);
}

/**
* Notice: onChange will be triggered twice on some devices when a sms received,
* eg: samsung s7 edge(API.23) - twice
* samsung note3(API.18) - once
* 06-15 11:45:48.706 D/SmsContent: onChange(boolean selfChange, Uri uri). selfChange=false, uri=content://sms/raw
* 06-15 11:45:49.466 D/SmsContent: onChange(boolean selfChange, Uri uri). selfChange=false, uri=content://sms/387
*
* Generally onChange will be triggered twice, first time is triggered by uri "content://sms/raw"(sms received,
* but have not written into inbox), second time is triggered by uri "content://sms/387"(number is sms id)
*
* Android official comments:
* This method is called when a content change occurs.
* Includes the changed content Uri when available.
*


* Subclasses should override this method to handle content changes.
* To ensure correct operation on older versions of the framework that
* did not provide a Uri argument, applications should also implement
* the {@link #onChange(boolean)} overload of this method whenever they
* implement the {@link #onChange(boolean, Uri)} overload.
*


* Example implementation:
*


* // Implement the onChange(boolean) method to delegate the change notification to
* // the onChange(boolean, Uri) method to ensure correct operation on older versions
* // of the framework that did not have the onChange(boolean, Uri) method.
* {@literal @Override}
* public void onChange(boolean selfChange) {
* onChange(selfChange, null);
* }
*
* // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
* {@literal @Override}
* public void onChange(boolean selfChange, Uri uri) {
* // Handle change.
* }
*

*


*
* @param selfChange True if this is a self-change notification.
* @param uri The Uri of the changed content, or null if unknown.
*/
@Override
public void onChange(boolean selfChange, Uri uri) {
Log.d(TAG, "onChange(boolean selfChange, Uri uri). selfChange=" + selfChange + ", uri=" + uri);

/**
* 适配某些较旧的设备,可能只会触发onChange(boolean selfChange)方法,没有传回uri参数,
* 此时只能通过"content://sms/inbox"来查询短信
*/
if (uri == null) {
uri = Uri.parse("content://sms/inbox");
}
/**
* 06-15 11:45:48.706 D/SmsContent: onChange(boolean selfChange, Uri uri). selfChange=false, uri=content://sms/raw
* 06-15 11:45:49.466 D/SmsContent: onChange(boolean selfChange, Uri uri). selfChange=false, uri=content://sms/387
*
* Generally onChange will be triggered twice, first time is triggered by uri "content://sms/raw"(sms received,
* but have not written into inbox), second time is triggered by uri "content://sms/387"(number is sms id)
*/
if (uri.toString().equals("content://sms/raw")) {
return;
}
cursor = this.mActivity.getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int id = cursor.getInt(cursor.getColumnIndex("_id"));
String body = cursor.getString(cursor.getColumnIndex("body"));
Log.d(TAG, "sms id: " + id + "\nsms body: " + body);
cursor.close();

// Already got sms body, do anything you want, for example: filter the verify code
getVerifyCode(body);
}
}
else {
Log.e(TAG, "error: cursor == null");
}
}

/**
* Register a monitor of changing of sms
*/
public void register() {
Log.d(TAG, "Register sms monitor");
this.mActivity.getContentResolver().registerContentObserver(
Uri.parse("content://sms/"), true, this);
}

/**
* Unregister the monitor of changing of sms
*/
public void unRegister() {
Log.d(TAG, "Unregister sms monitor");
this.mActivity.getContentResolver().unregisterContentObserver(this);
}

/**
* Get verify code from sms body
* @param str
* @return
*/
public String getVerifyCode(String str) {
String verifyCode = null;
if (smsContentFilter(str)) {
Log.d(TAG, "sms content matched, auto-fill verify code.");
verifyCode = getDynamicPassword(str);
}
else {
// Do nothing
Log.d(TAG, "sms content did not match, do nothing.");
}
return verifyCode;
}

/**
* Check if str is verification-code-formatted
*
* @param str
* @return
*/
private boolean smsContentFilter(String str) {
Log.d(TAG, "smsContentFilter. smsBody = " + str);
boolean isMatched = false;
if (!TextUtils.isEmpty(str)) {
// Check if str contains keyword
if (str.contains(MARKER)) {
Log.d(TAG, "This sms contains \"" + MARKER + "\"");
// Check if str contains continuous 6 numbers
Pattern continuousNumberPattern = Pattern.compile("[0-9\\.]+");
Matcher m = continuousNumberPattern.matcher(str);
while(m.find()){
if(m.group().length() == 6) {
Log.d(TAG, "This sms contains continuous 6 numbers : " + m.group());
isMatched = true;
}
}
}
}
return isMatched;
}

/**
* Cut the continuous 6 numbers from str
*
* @param str sms content
* @return verification code
*/
private String getDynamicPassword(String str) {
Log.d(TAG, "getDynamicPassword. smsBody = " + str);
Pattern continuousNumberPattern = Pattern.compile("[0-9\\.]+");
Matcher m = continuousNumberPattern.matcher(str);
String dynamicPassword = "";
while(m.find()){
if(m.group().length() == 6) {
Log.d(TAG, m.group());
dynamicPassword = m.group();
}
}

Log.d(TAG, "Verification code: " + dynamicPassword);
return dynamicPassword;
}
}

用法很简单,注册和注销一下就行:

package com.jackie.bindmobile;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;

public class BindMobileActivity extends Activity {

private static final String TAG = BindMobileActivity.class.getSimpleName();
private SmsContent smsContent;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Register sms monitor
smsContent = new SmsContent(new Handler(), mActivity);
smsContent.register();
}

@Override
protected void onDestroy() {
super.onDestroy();
// Unregister sms monitor
smsContent.unRegister();
}
}

[size=large][b]方法二:广播[/b][/size]

public class SMSBroadcastReceiver extends BroadcastReceiver {

private static final String TAG = "SMSBroadcastReceiver";

private SMSBroadcastReceiver() {
}

public static SMSBroadcastReceiver buildSMSReceiver(Context context) {
SMSBroadcastReceiver smsReceiver = new SMSBroadcastReceiver();
IntentFilter iff = new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
iff.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
context.registerReceiver(smsReceiver, iff);
return smsReceiver;
}

public static void unregisterSMSReceiver(Context context, SMSBroadcastReceiver receiver) {
context.unregisterReceiver(receiver);
}

@SuppressLint("SimpleDateFormat")
@Override
public void onReceive(Context context, Intent intent) {
Object[] pdus = (Object[]) intent.getExtras().get("pdus");
for (Object pdu : pdus) {
SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) pdu);
String sender = smsMessage.getDisplayOriginatingAddress();
String content = smsMessage.getMessageBody();
long date = smsMessage.getTimestampMillis();
Date timeDate = new Date(date);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = simpleDateFormat.format(timeDate);
Log.i(TAG, " ------------- Incoming message details start ------------- ");
Log.i(TAG, "SMS from: " + sender);
Log.i(TAG, "SMS body: " + content);
Log.i(TAG, "SMS timestamp: " + time);
LogCat.i(TAG, " ------------- Incoming message details end ------------- ");
// Mark SMS as read
markAsRead(context, sender);
}
}

private void markAsRead(final Context context, final String sender) {
// Can not get the latest sms from ContentResolver on Android4.3 device if not run in sub thread
new Thread(new Runnable() {
@Override
public void run() {
// Wait for mobile system to write sms into database
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Log.e(TAG, e.getMessage());
}
// Query sms from database
Uri uri = Uri.parse("content://sms/inbox");
String where = " address = " + sender;
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(uri, null, where, null, "_id desc");
if (null == cursor) return;
if (cursor.moveToNext()) {
// Query status
String smsId = cursor.getString(cursor.getColumnIndex("_id"));
int read = cursor.getInt(cursor.getColumnIndex("read"));
String smsBody = cursor.getString(cursor.getColumnIndex("body"));
Log.d(TAG, "Before update. smsId=" + smsId + ", read=" + read + ", smsBody=" + smsBody);
// Mark as read
ContentValues values = new ContentValues();
values.put("read", 1);
resolver.update(uri, values, "_id=" + smsId, null);
Log.d(TAG, "Mark as read DONE");
// Confirm status
cursor = resolver.query(uri, null, "_id=" + smsId, null, null);
cursor.moveToNext();
smsId = cursor.getString(cursor.getColumnIndex("_id"));
read = cursor.getInt(cursor.getColumnIndex("read"));
smsBody = cursor.getString(cursor.getColumnIndex("body"));
Log.d(TAG, "After update. smsId=" + smsId + ", read=" + read + ", smsBody=" + smsBody);
}
cursor.close();
}
}).start();
}
}

用法也很简单,比如你要在一个Service中使用,只要注册和注销即可:

class SMSService extends Service{

private SMSBroadcastReceiver smsBroadcastReceiver;

@Override
private void onCreate() {
smsBroadcastReceiver = SMSBroadcastReceiver.buildSMSReceiver(this);
}

@Override
public void onDestroy() {
SMSBroadcastReceiver.unregisterSMSReceiver(this, smsBroadcastReceiver);
super.onDestroy();
}
}

上面的例子实现的功能是接收短信广播,收到后将短信设置为已读,实现过程是当触发onReceive后,通过ContentResolver读取短信数据库,并更新相应短信的状态为已读。需要注意里面两个地方:
1.为什么要做3秒的延迟,再去读取短信数据库:
是为了等待手机系统将短信写入短信数据库中,否则你读到的就是旧的数据库信息,不包含刚收到的短信。关于这一点,我的理解是:广播机制它是系统一旦收到短信,就发出一个广播,并将短信内容以pdus的形式放在Intent中,同时系统又去将短信往数据库中写入,发广播和写数据库是并行进行的,他们之间没有什么先后关系,所以当我们收到onReceive时其实很可能短信还没被写入数据库,如果此时去读取数据库,就读不到最新的这条短信。当然我没有去证实这个观点,但应该是八九不离十。
2.更新短信状态的处理为什么要放在子线程:
这个就只是为了适配老设备,起初没有放在子线程中,我在红米(Android4.4.4)上测试时没有问题,只要延时3秒就能读到最新的短消息,但是在一台很老的联想机(Android4.3)上面就总是读到的之前一条短信内容,即便我将延时设置为30秒也没用,我不确定是设备太老还是系统版本太旧,但我偶然发现将这个处理放在子线程后3秒就可以读到最新的短信。如果有大神能解释一下,请多多指教。

其实,方法二这样的功能,最好还是使用第一种方式监听短信,因为只有第一种方式能够写短信内容,而广播只能读短信,如果硬要将两种方式结合起来用,那在读取短信数据库前就不得不做一定的延时处理了。

你可能感兴趣的:(Android开发,前端开发)