Android SMS 수신 기능(onChange에서 두 번 발생하는 문제 해결)

자세히 보기
전언
프로젝트는 문자 인증 코드를 자동으로 채우는 기능을 해야 한다. 기본적으로 두 가지 방법이 있는데 그것이 바로 콘텐츠 Observer가 문자 데이터베이스, 방송을 감청하는 것이다.저는 개인적으로 데이터베이스를 감청하는 방식을 좋아합니다. 그 이유는 다음과 같습니다.
1. 방송 방식은 문자만 읽을 수 있고 문자를 쓸 수 없다(예를 들어 문자를 업데이트한 상태는 이미 읽은 상태이다).감청 데이터베이스는 문자메시지에 대해 첨삭 검사를 할 수 있다.
2. 시스템의 문자 방송은 질서정연한 방송이다. 만약에 다른 응용 프로그램의 우선순위가 우리의 응용 프로그램보다 높으면 방송은 먼저 다른 응용 프로그램에 의해 캡처된다. 만약에 다른 응용 프로그램이 문자 방송을 캡처한 후에 이를 소비하고 더 이상 발송하지 않으면 우리의 응용 프로그램은 방송을 받지 못한다.
3. 방송 방식은receiver에 등록해야 한다. 동적 등록은 괜찮다. 만약에 정적 등록이라면 sdk류 제품에 대해 사용자는 Manifest에 등록하는 것을 잊어버릴 수 있다.
상기 원인을 바탕으로 제 프로젝트에서 아예 감청 데이터베이스를 선택했습니다.감청 데이터베이스는 방송에 비해 조금 복잡하지만 더욱 유연하고 기능이 강하기 때문에 감청 데이터베이스를 사용하는 방법을 권장하지만 두 가지를 모두 말씀드리겠습니다.
방법1: 감청 문자 데이터베이스
대부분의 휴대전화는 문자 한 통을 받을 때 온체인지(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 + "sms 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();
    }
}

방법2: 방송

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();
    }
}

사용법도 간단하다. 예를 들어 한 서비스에서 사용하려면 등록과 취소만 하면 된다.

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를 터치한 후Content Resolver를 통해 문자 데이터베이스를 읽고 해당하는 문자 메시지를 업데이트한 상태를 읽은 것으로 한다.안쪽 두 군데를 주의해야 한다.
1. 왜 3초의 딜레이를 하고 문자 데이터베이스를 읽어야 하는가:
휴대전화 시스템이 문자메시지를 문자 데이터베이스에 쓰기를 기다리기 위해서다. 그렇지 않으면 당신이 읽은 것은 오래된 데이터베이스 정보이며 방금 받은 문자메시지는 포함되지 않는다.이 점에 대해 제 이해는 방송 메커니즘은 시스템이 문자를 받으면 방송을 하고 문자 내용을 pdus의 형식으로 Intent에 놓는 동시에 시스템은 문자를 데이터베이스에 기록하고 방송과 데이터베이스를 병행하여 진행하는 것입니다. 그들 사이에는 선후 관계가 없습니다.그래서 우리가 onReceive를 받았을 때 사실 문자가 데이터베이스에 기록되지 않았을 가능성이 높다. 만약에 데이터베이스를 읽으러 가면 최신 문자를 읽을 수 없다.물론 나는 이 관점을 증명하지 않았지만, 십중팔구는 틀림없을 것이다.
2. 문자 상태를 업데이트하는 처리는 왜 하위 라인에 두어야 하는가:
이것은 단지 오래된 설비에 맞게 처음에 서브라인에 넣지 않았기 때문에 나는 홍미(Android4.4)에서 테스트할 때 문제가 없었다. 3초만 늦추면 최신 짧은 소식을 읽을 수 있었지만 오래된 연상기(Android4.3)에서 항상 읽은 앞의 문자 내용은 내가 30초로 설정해도 소용없었다.나는 설비가 너무 낡았는지 시스템 버전이 너무 낡았는지 확실하지 않지만, 나는 우연히 이 처리를 하위 라인에 놓은 후 3초 후에 최신 문자를 읽을 수 있다는 것을 발견했다.만약 신이 설명해 줄 수 있다면 많이 가르쳐 주십시오.
사실 방법2와 같은 기능은 첫 번째 방식으로 문자를 감청하는 것이 가장 좋다. 첫 번째 방식만 문자의 내용을 쓸 수 있고 방송은 문자만 읽을 수 있기 때문이다. 만약에 두 가지 방식을 억지로 결합시켜 쓰려고 한다면 문자의 데이터베이스를 읽기 전에 일정한 시간 지연 처리를 해야 한다.

좋은 웹페이지 즐겨찾기