문자 인증번호를 앱이 자동으로 읽어야 하거나, 특정 번호에서 온 문자를 받아 서버로 넘겨야 할 때가 있습니다. 안드로이드에서는 문자가 도착하면 시스템이 android.provider.Telephony.SMS_RECEIVED 라는 브로드캐스트를 뿌리는데, 이걸 받아 줄 리시버만 등록해 두면 됩니다. 아래 예제는 받기와 보내기를 한 묶음으로 담고 있습니다.

매니페스트에 넣어야 할 것

먼저 권한입니다. 문자를 받으려면 RECEIVE_SMS 가 필요하고, 예제에서는 전화 상태 읽기 권한도 같이 선언했습니다.

광고
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

그다음 <application> 안에 리시버를 등록합니다. 인텐트 필터의 액션 이름이 정확해야 시스템이 이 리시버를 깨워 줍니다.

<receiver android:name=".SMSBroadcast">
    <intent-filter>
        <action android:name="android.provider.Telephony.SMS_RECEIVED" />
    </intent-filter>
</receiver>

주의할 점이 하나 있습니다. 여기 android:name 은 .SMSBroadcast 인데, 아래에서 실제로 만드는 클래스 이름은 SmsReceiver 입니다. 그대로 붙여 넣으면 리시버 클래스를 찾지 못하니 둘 중 하나로 이름을 맞춰 주세요. 그리고 이 예제는 문자 발송도 하므로 android.permission.SEND_SMS 권한 선언도 함께 넣어야 실제로 보내집니다.

받은 문자를 꺼내는 리시버

리시버가 받는 인텐트의 extras 에는 "pdus" 라는 키로 PDU 바이트 배열 묶음이 들어 있습니다. 긴 문자는 여러 조각으로 나뉘어 오기 때문에 배열입니다. 각 조각을 SmsMessage.createFromPdu() 로 변환하면 보낸 번호(getOriginatingAddress())와 본문(getMessageBody())을 꺼낼 수 있습니다. 예제는 그 내용을 문자열로 이어 붙여 토스트로 띄웁니다.

public class SmsReceiver extends BroadcastReceiver {

    public void onReceive(Context context, Intent intent) {
        Bundle bundle = intent.getExtras();
        SmsMessage[] msgs = null;
        String str = "";
        if (bundle != null)
        {
            Object[] pdus = (Object[]) bundle.get("pdus");
            msgs = new SmsMessage[pdus.length];
            for (int i=0; i<msgs.length; i++){
                msgs[i] = SmsMessage.createFromPdu((byte[])pdus[i]);
                str += "SMS from " + msgs[i].getOriginatingAddress();
                str += " :";
                str += msgs[i].getMessageBody().toString();
                str += "\n";
            }
            Toast.makeText(context, str, Toast.LENGTH_SHORT).show();
        }
    }

}

실제로 쓸 때는 토스트 대신 번호를 걸러 내고 본문에서 인증번호만 정규식으로 뽑아 쓰는 경우가 대부분입니다. onReceive 는 메인 스레드에서 짧게 끝나야 하므로, 서버 전송 같은 무거운 작업은 여기서 직접 하지 말고 별도 작업으로 넘기는 편이 안전합니다.

문자 보내기와 결과 확인

보내는 쪽은 SmsManager.sendTextMessage() 한 줄이면 됩니다. 다만 "정말 나갔는지", "상대에게 도착했는지"를 알고 싶다면 PendingIntent 두 개를 넘기고, 그 결과를 받을 리시버를 동적으로 등록해야 합니다. 발송 결과는 getResultCode() 로 확인하는데, 실패 사유가 일반 오류·서비스 없음·Null PDU·비행기 모드(라디오 꺼짐)로 나뉘어 들어옵니다. 도착 결과는 RESULT_OK 와 RESULT_CANCELED 두 가지입니다.

결과가 필요 없다면 아래쪽 __sendSMS 처럼 간단하게 보내도 됩니다.

public class SmsSender extends Activity {

    Button btnSendSMS;
    EditText txtPhoneNo;
    EditText txtMessage;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.sms_send);

        btnSendSMS = (Button) findViewById(R.id.button);
        txtPhoneNo = (EditText) findViewById(R.id.phone);
        txtMessage = (EditText) findViewById(R.id.message);

                btnSendSMS.setOnClickListener(new View.OnClickListener()
                {
                    public void onClick(View v)
                    {
                        String phoneNo = txtPhoneNo.getText().toString();
                        String message = txtMessage.getText().toString();
                        if (phoneNo.length()>0 && message.length()>0)
                            sendSMS(phoneNo, message);
                        else
                            Toast.makeText(getBaseContext(),
                                    "Please enter both phone number and message.",
                                    Toast.LENGTH_SHORT).show();
                    }
                });
    }

    // 발송·도착 과정을 확인하고 싶을 때
    private void sendSMS(String phoneNumber, String message)
    {
        String SENT = "SMS_SENT";
        String DELIVERED = "SMS_DELIVERED";

        PendingIntent sentPI = PendingIntent.getBroadcast(this, 0,
                new Intent(SENT), 0);

        PendingIntent deliveredPI = PendingIntent.getBroadcast(this, 0,
                new Intent(DELIVERED), 0);

        //---발송 완료 시---
        registerReceiver(new BroadcastReceiver(){
            @Override
            public void onReceive(Context arg0, Intent arg1) {
                switch (getResultCode())
                {
                    case Activity.RESULT_OK:
                        Toast.makeText(getBaseContext(), "SMS sent",
                                Toast.LENGTH_SHORT).show();
                        break;
                    case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
                        Toast.makeText(getBaseContext(), "Generic failure",
                                Toast.LENGTH_SHORT).show();
                        break;
                    case SmsManager.RESULT_ERROR_NO_SERVICE:
                        Toast.makeText(getBaseContext(), "No service",
                                Toast.LENGTH_SHORT).show();
                        break;
                    case SmsManager.RESULT_ERROR_NULL_PDU:
                        Toast.makeText(getBaseContext(), "Null PDU",
                                Toast.LENGTH_SHORT).show();
                        break;
                    case SmsManager.RESULT_ERROR_RADIO_OFF:
                        Toast.makeText(getBaseContext(), "Radio off",
                                Toast.LENGTH_SHORT).show();
                        break;
                }
            }
        }, new IntentFilter(SENT));

        //---상대방에게 도착 시---
        registerReceiver(new BroadcastReceiver(){
            @Override
            public void onReceive(Context arg0, Intent arg1) {
                switch (getResultCode())
                {
                    case Activity.RESULT_OK:
                        Toast.makeText(getBaseContext(), "SMS delivered",
                                Toast.LENGTH_SHORT).show();
                        break;
                    case Activity.RESULT_CANCELED:
                        Toast.makeText(getBaseContext(), "SMS not delivered",
                                Toast.LENGTH_SHORT).show();
                        break;
                }
            }
        }, new IntentFilter(DELIVERED));

        SmsManager sms = SmsManager.getDefault();
        sms.sendTextMessage(phoneNumber, null, message, sentPI, deliveredPI);
    }

    // 결과 확인 없이 보내기만 할 때
    private void __sendSMS(String phoneNumber, String message)
    {
        PendingIntent pi = PendingIntent.getActivity(this, 0,
                new Intent(this, SmsSender.class), 0);
        SmsManager sms = SmsManager.getDefault();
        sms.sendTextMessage(phoneNumber, null, message, pi, null);
    }

}

sendSMS 를 부를 때마다 registerReceiver 가 새로 호출되는 구조라서, 버튼을 여러 번 누르면 리시버가 계속 쌓이고 토스트도 그만큼 중복으로 뜹니다. 실제 앱에 넣는다면 리시버를 필드로 한 번만 만들어 onResume 에서 등록하고 onPause 에서 unregisterReceiver 로 풀어 주는 식으로 정리하는 게 좋습니다.

화면 레이아웃

번호 입력칸, 메시지 입력칸, 전송 버튼만 있는 단순한 화면입니다. 원래 글에서는 이 파일을 receive_sms.xml 로 만든다고 했는데, 액티비티 코드는 R.layout.sms_send 를 찾고 있으니 파일명을 sms_send.xml 로 저장하거나 코드 쪽을 바꿔 맞춰야 합니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/widget28"
    android:layout_width="fill_parent" android:layout_height="fill_parent"
    android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView android:id="@+id/label1" android:layout_width="fill_parent"
        android:layout_height="wrap_content" android:text="받는사람 전화번호"
        android:textSize="12sp">
    </TextView>
    <EditText android:id="@+id/phone" android:layout_width="fill_parent"
        android:layout_height="wrap_content" android:textSize="18sp"
        android:phoneNumber="true">
    </EditText>
    <TextView android:id="@+id/label2" android:layout_width="fill_parent"
        android:layout_height="wrap_content" android:text="메시지"
        android:textSize="12sp">
    </TextView>
    <EditText android:id="@+id/message" android:layout_width="fill_parent"
        android:layout_height="150px" android:gravity="top">
    </EditText>
    <Button android:id="@+id/button" android:layout_width="fill_parent"
        android:layout_height="wrap_content" android:text="전송">
    </Button>
    </LinearLayout>

요즘 기기에서 돌릴 때 막히는 부분

이 코드는 꽤 오래된 방식이라 최신 안드로이드에서 그대로 돌리면 몇 군데서 걸립니다. 안드로이드 6.0 이상은 SMS 권한이 위험 권한이라 매니페스트 선언만으로는 부족하고, 실행 중에 사용자에게 권한 요청을 띄워 허락을 받아야 합니다. 또 안드로이드 12부터는 PendingIntent 를 만들 때 플래그를 0 으로 주면 예외가 나므로 PendingIntent.FLAG_IMMUTABLE 같은 값을 지정해야 합니다. fill_parent 는 지금은 match_parent 로 쓰는 게 일반적이지만 동작은 같습니다.

구글 플레이에 올릴 앱이라면 SMS 권한 자체가 정책 심사 대상이라는 점도 알고 있어야 합니다. 인증번호 자동 입력이 목적이라면 SMS 권한 없이 쓸 수 있는 SMS Retriever API 쪽을 먼저 검토해 보는 걸 권합니다. 사내용 단말이나 직접 설치해서 쓰는 앱이라면 위 코드 구조로도 충분히 쓸 만합니다.