문자 인증번호를 앱이 자동으로 읽어야 하거나, 특정 번호에서 온 문자를 받아 서버로 넘겨야 할 때가 있습니다. 안드로이드에서는 문자가 도착하면 시스템이 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 쪽을 먼저 검토해 보는 걸 권합니다. 사내용 단말이나 직접 설치해서 쓰는 앱이라면 위 코드 구조로도 충분히 쓸 만합니다.