브로드캐스팅(Broadcasting) : 메시지를 여러 객체에 전달

글로벌 이벤트(Global Event) : 메시지를 브로드캐스팅으로 전달하는 방식 (ex. 전화, 문자에 대한 사용자 알림 메시지)

 

브로드캐스팅 메시지를 받고 싶으면 브로드캐스트 수신자를 만들어 앱에 등록한다. (registerReceiver(), onReceive())

다른 앱의 메시지를 전달받을 수 있음.

이 중 원하는 메시지만 받으려면 인텐트 필터를 사용하여 시스템에 등록하면 된다.

 

브로드캐스트 수신자의 특징 : 앱을 실행하지 않은 상태에서도 인텐트 안에 들어있는 메시지를 받아볼 수 있다.

 

브로드캐스트 수신자를 포함하고 있는 메인 액티비티가 적어도 한번 실행되어야 브로드캐스트 수신자가 메시지를 받을 수 있다.

 

브로드캐스트를 등록하는 방법은 2가지가 있다.

  • 매니패스트 파일에 등록 : 언제든 브로드캐스트 수신자에서 메시지를 받음.
  • 소스 파일에서 등록 : 화면이 사용자에게 보일 때만 브로드캐스트 수신자에서 메시지를 받도록 만들 수 있음.
sendBroadcast() : 다른 앱에 메시지를 보내고 싶을 경우 이 메서드를 사용한다. (이번 예제에서는 안씀)

 

<SMS 문자 받은 뒤 발신번호, 문자내용, 받은시각 액티비티에 띄우기>

1. 새 프로젝트를 만들고 브로드캐스트 수신자를 생성한다.

더보기

브로드캐스트 수신자 이름은 SMSReceiver로 설정함.

2. 매니패스트 파일을 수정한다.

더보기

AndroidManifest.xml에 들어간다.

앱에서 SMS를 수신하기 위해 권한을 추가해야한다. 아래 코드를 입력한다.

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

 

SMS 메시지가 들어간 인텐트를 구분하기 위한 액션 정보를 추가한다. android.provider.Telephony.SMS_RECEIVED

<reciever> 태그 안에 아래 코드를 입력한다.

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

 

3. SMSReceiver.java 작성 (브로드캐스트 수신자)

더보기
package com.example.chaoter6_2;

public class SMSReceiver extends BroadcastReceiver {
    public static final String TAG = "SMSReciever";
    public SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
    // receivedDate 양식

    @Override
    public void onReceive(Context context, Intent intent) {
    // 파라미터로 들어오는 인텐트 객체 안에 SMS 데이터가 있음.
        // TODO: This method is called when the BroadcastReceiver is receiving

        Log.d(TAG,"OnReceive() 메서드 호출됨.");

        Bundle bundle = intent.getExtras(); // 번들 객체 안에 부가 데이터가 들어감
        SmsMessage[] messages = parseSmsMessage(bundle);
        // sms 메시지를 뽑아내기 위해 만든 메서드 parsSmsMessage

        // 입력이 들어올 경우
        if(messages!=null && messages.length>0){
            // 발신자 번호 가져오기
            String sender = messages[0].getOriginatingAddress(); 
            Log.i(TAG, "SMS sender : " +sender.toString());
            // 문자 내용 가져오기
            String contents = messages[0].getMessageBody(); 
            Log.i(TAG,"SMS contents : "+contents.toString());
            // SMS 받은 시각 가져오기
            Date receivedDate = new Date(messages[0].getTimestampMillis());
            Log.i(TAG,"SMS received date : "+receivedDate.toString());

            sendToActivity(context, sender, contents, receivedDate);
        }

        //throw new UnsupportedOperationException("Not yet implemented");
    }

    private SmsMessage[] parseSmsMessage(Bundle bundle){
        Object[] objs = (Object[]) bundle.get("pdus"); // 부가 데이터 중 pdus 가져오기
        SmsMessage[] messages = new SmsMessage[objs.length];

        int smsCount = objs.length;
        for(int i=0;i<smsCount;i++){
            // 단말 OS 버전에 따라 다른 방식으로 메서드 호출.
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
            // OS가 마시멜로(M) 버전과 같거나 이후 버전일 경우
                String format = bundle.getString("format");
                messages[i]=SmsMessage.createFromPdu((byte[]) objs[i], format);
            }
            else
                messages[i]=SmsMessage.createFromPdu((byte[]) objs[i]);
        }
        return messages;
    }

    private void sendToActivity(Context context, String sender, String contents, Date receivedDate){
        Intent myIntent = new Intent(context, SMSActicity.class); //SMSActivity로 인텐트 보내기

        myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK| // 브캐 수신자는 화면이 없으므로 화면을 생성하기 위해 추가
                Intent.FLAG_ACTIVITY_SINGLE_TOP| // 이미 메모리에 만든 SMSActivity가 있을 때 액티비티 중복생성하지 않기
                Intent.FLAG_ACTIVITY_CLEAR_TOP);

        myIntent.putExtra("sender",sender);
        myIntent.putExtra("contents",contents);
        myIntent.putExtra("receivedDate",format.format(receivedDate));

        context.startActivity(myIntent);
    }
}
  • SimpleDateFormat
더보기
public class SMSReceiver extends BroadcastReceiver {
    public static final String TAG = "SMSReciever";
    public SimpleDateFormat format = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
    // receivedDate 양식
    ...

SimpleDateFormat은 날짜와 시간을 원하는 형태의 문자열로 만들 때 사용한다.

  • onRecieve()
더보기
    @Override
    public void onReceive(Context context, Intent intent) {
    // 파라미터로 들어오는 인텐트 객체 안에 SMS 데이터가 있음.
        // TODO: This method is called when the BroadcastReceiver is receiving

        Log.d(TAG,"OnReceive() 메서드 호출됨.");

        Bundle bundle = intent.getExtras(); // 번들 객체 안에 부가 데이터가 들어감
        SmsMessage[] messages = parseSmsMessage(bundle); // sms 메시지를 뽑아내기 위해 만든 메서드

        // 입력이 들어올 경우
        if(messages!=null && messages.length>0){
            String sender = messages[0].getOriginatingAddress(); // 발신자 번호 가져오기
            Log.i(TAG, "SMS sender : " +sender.toString());

            String contents = messages[0].getMessageBody(); // 문자 내용 가져오기
            Log.i(TAG,"SMS contents : "+contents.toString());

            Date receivedDate = new Date(messages[0].getTimestampMillis()); // SMS 받은 시각 가져오기
            Log.i(TAG,"SMS received date : "+receivedDate.toString());

            sendToActivity(context, sender, contents, receivedDate);
        }

        //throw new UnsupportedOperationException("Not yet implemented");
    }

 

SMS를 뽑아내기 위한 메서드 parseSmsMessage가 초기에 실행되어 messages에 문자정보가 들어간다.

 

문자 정보는 messages[0]에서 접근가능한가봄.

sender(수신번호) ← getOriginatingAddress()

contents(문자내용) ← getMessageBody()

receivedDate(받은시각) ← getTimestampMillis()

이 중에서 receivedDate만 String 형식이 아닌 Date 형식인 것에 주의. (Date는 자동으로 import 되지 않아서 빨간색으로 표시된다. Alt+Enter를 누르고 java.util 패키지 안에 있는 것을 선택하자)

 

메서드 sendToActivity가 실행되어 위의 문자 정보가 파라미터로 전달되어, 액티비티로 전달됨.

 

마지막 //주석 부분은 자동으로 생성된 코드인데 에러가 발생해서 지움. (메시지 알림이 뜨면 앱이 강제종료됨)

에러 발생

https://stackoverflow.com/questions/14493907/android-unable-to-start-receiver-not-yet-implemented

  • parseSmsMessage() : SMS를 뽑아내기 위한 메서드
더보기
    private SmsMessage[] parseSmsMessage(Bundle bundle){
        Object[] objs = (Object[]) bundle.get("pdus"); // 부가 데이터 중 pdus 가져오기
        SmsMessage[] messages = new SmsMessage[objs.length];

        int smsCount = objs.length;
        for(int i=0;i<smsCount;i++){
            // 단말 OS 버전에 따라 다른 방식으로 메서드 호출.
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ 
            // OS가 마시멜로(M) 버전과 같거나 이후 버전일 경우
                String format = bundle.getString("format");
                messages[i]=SmsMessage.createFromPdu((byte[]) objs[i], format);
            }
            else
                messages[i]=SmsMessage.createFromPdu((byte[]) objs[i]);
        }
        return messages;
    }
  • createFromPdu()

   인텐트 객체 안에 부가 데이터로 들어있는 SMS 데이터를 확인하려면 SmsMessage 클래스의 createFromPdu

   메서드를 사용하여 SmsMessage 객체로 변환하면 된다.

  • Build.VERSION.SDK_INT

   단말의 OS 버전을 확인할 때 사용함

  • Build.VERSION.CODE

   안드로이드 OS 버전별로 상수가 정의되어 있다.

  • sendToActivity() : 인텐트를 사용하여 액티비티로 문자 정보를 보내는 메서드
더보기
private void sendToActivity(Context context, String sender, String contents, Date receivedDate){
    Intent myIntent = new Intent(context, SMSActicity.class); //SMSActivity로 인텐트 보내기

    myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK| // 브캐 수신자는 화면이 없으므로 화면을 생성하기 위해 추가
            Intent.FLAG_ACTIVITY_SINGLE_TOP| // 이미 메모리에 만든 SMSActivity가 있을 때 액티비티 중복생성하지 않기
            Intent.FLAG_ACTIVITY_CLEAR_TOP);
            // Task 내에 해당 속성이 적용된 activity부터 top activity까지 모두 제거한뒤
            // 해당 activity를 활성화 하여 top이 되도록 한다.

    myIntent.putExtra("sender",sender);
    myIntent.putExtra("contents",contents);
    myIntent.putExtra("receivedDate",format.format(receivedDate));

    context.startActivity(myIntent);
}

 아직 context의 개념을 잘 모르겠다.

4. build.gradle

더보기

변경된 부분을 따라서 작성한다.

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
dependencies {
    constraints {
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") {
            because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
        }
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") {
            because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
        }
    }

    implementation ("com.github.pedroSG94:AutoPermissions:1.0.3")
    implementation(libs.activity)
    testImplementation(libs.junit)
    androidTestImplementation(libs.ext.junit)
    androidTestImplementation(libs.espresso.core)
    implementation("androidx.core:core:1.13.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.9.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
}

 이 부분은 특히 아주 그지 같았던 부분이다. 설명은 아래 글을 보자

2025.01.05 - [TIL/안드로이드 스튜디오] - duplicate class 에러 (JDK7, JDK8 충돌)

 

duplicate class 에러 (JDK7, JDK8 충돌)

아 진짜 엿같았던 에러.. 아직도 뭐 때문에 문제가 발생했는지 정확하게는 잘 모르겠으나권한요청을 구현하기 위해 새로운 라이브러리를 implement해서 발생한 듯. 어떻게 해결했는지 그 과정.

sand-to-desert.tistory.com

 

5. gradle.properties 

더보기
android.enableJetifier=true

6. settings.gradle

더보기

url을 추가.

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven {url = uri ("https://www.jitpack.io")}
    }
}

7. MainActivity 작성

더보기

어.. 지금 다시 보니까 내가 책이랑 다르게 작성을 했다. 근데 책에도 다음 목차에서 다루는 내용이라서 자세한 설명은 패스했다. 일단 이 부분은 그냥 대충 훑어보고 넘어가자.

package com.example.chaoter6_2;

public class MainActivity extends AppCompatActivity implements AutoPermissionsListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        AutoPermissions.Companion.loadAllPermissions(this, 101);
    }

    @Override
    public void onGranted(int requestCode, @NonNull String[] permissions) {
        Toast.makeText(this, "permissions granted : " + permissions.length, Toast.LENGTH_LONG).show();
    }

    @Override
    public void onDenied(int requestCode, @NonNull String[] permissions) {
        Toast.makeText(this, "permissions denied : " + permissions.length, Toast.LENGTH_LONG).show();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        AutoPermissions.Companion.parsePermissions(this, requestCode, permissions, this);
        Toast.makeText(this,"requestCode : "+requestCode+", permissions : "+permissions+", grantResults : "+grantResults, Toast.LENGTH_LONG).show();
    }
}

8. 새로운 액티비티를 생성한다. (SMSActivity)

더보기

브로드캐스트 수신자에서 인텐트 객체를 만들고 startActivity 메서드를 사용해 액티비티 쪽으로 인텐트 객체를 전달한다. 여기서 전달받을 액티비티를 만든다.

  • activity_sms.xml
더보기

여기서 editTextText2는 입력이 길어질 경우 자동으로 줄바꿈이 될 수 있도록 아래 코드를 추가한다.

        android:inputType="textMultiLine"
        android:scrollHorizontally="false"
  • SMSActivity.java
더보기
package com.example.chaoter6_2;

public class SMSActivity extends AppCompatActivity {

    EditText editText_num, editText_content, editText_time;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sms);

        editText_num = findViewById(R.id.editTextText);
        editText_content = findViewById(R.id.editTextText2);
        editText_time = findViewById(R.id.editTextText3);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });
        
        // 전달받은 인텐트를 처리하도록 processIntent 메서드 호출하기
        Intent passedIntent = getIntent();
        processIntent(passedIntent);
    }

    @Override
    protected void onNewIntent(Intent intent) {
        processIntent(intent);
        super.onNewIntent(intent);
    }

    private void processIntent(Intent intent){
        if(intent!=null){ // 인텐트 안에 들어 있는 부가 데이터를 꺼내서 입력상자에 설정한다.
            String sender = intent.getStringExtra("sender");
            String contents = intent.getStringExtra("contents");
            String receivedDate = intent.getStringExtra("receivedDate");

            editText_num.setText(sender);
            editText_content.setText(contents);
            editText_time.setText(receivedDate);
        }
    }
}
  • onClick() : 앱 종료
  • onNewIntent() : 재정의하여 이 액티비티가 이미 만들어져 있는 상태에서 전달받은 인텐트도 처리함
  • processIntent() : 인텐트 객체 안에 들어있는 부가 데이터를 꺼내서 입력상자에 설정

9. 가상 SMS 전송

더보기

AVD 화면에서 상단탭 중 가장 오른쪽에 위치한 점 3개를 누른다.

10. 실행결과

 

입력을 많이해도 한글 최대 67자까지 액티비티에 전달되었다. 

왜 그런가 알아보니 다음과 같았다.

 

11. 브로드캐스트 수신자 동작 방식 정리

더보기

1. SMS 문자를 받았을 때, 텔레포니(Telephony) 모듈이 처리하도록 한다.

2. 인텐트를 받았을 때 onReceive 메서드가 자동 호출

3. Receiver 객체에서 데이터 확인 후 Activity로 인텐트 전달

 

브로드캐스트 사용 시 주의할 점 : 앱 A가 실행되어 있지 않아도 앱 A가 원하는 브로드캐스트 메시지가 도착하면

                                                      다른 앱 B를 실행하고 있는 도중에도 앱 A가 실행될 수 있음.

                                                      즉 동일한 SMS 수신 앱을 여러 개 수정하여 만들어 설치하면 오류가 발생했을 때

                                                      어디서 발생했는지 찾기가 힘들다.

                                                      여러 버전의 앱이 깔려있을 때, 구 개발 버전의 앱은 삭제하는 것이 좋다.