Android - Broadcast Receiver 등록 및 이벤트 수신 방법

JS · 06 Jun 2020

Android는 Broadcast를 통해 시스템과 앱 또는 앱과 앱 간의 이벤트를 주고 받을 수 있습니다.

publish-subscribe 디자인 패턴처럼 한쪽에서는 이벤트를 제공하기만 하고 한쪽에서는 이벤트를 받기만 합니다.

예를 들어, Broacast를 통해 앱은 시스템으로 부터 Local이 변경되거나, App이 설치되는 등의 다양한 이벤트를 받을 수 있습니다. 또한, 앱들끼리도 미리 사전에 정의한 Action을 주고 받을 수 있습니다.

앱이 Broadcast를 받으려면 먼저 Receiver를 등록해야 합니다. BroadcastReceiver 객체를 생성하고 이것을 Manifest에 등록하거나 registerReceiver()로 등록해야 합니다. 그리고 sendBroadcast()를 이용하여 앱에서 브로드캐스트를 보낼 수 있습니다.

Broadcast 이벤트를 보내고 받는 방법에 대해서 자세히 알아보겠습니다.

정적 BroadcastReceiver 등록

정적 BroadcastReceiver는 앱을 설치하는 시점에 Receiver를 등록하는 것을 말합니다. Developer에는 Manifest-declared receivers 용어로 표현합니다.

  • 장점으로는 앱이 실행되지 않아도 리시버가 항상 등록되어있기 때문에 모든 이벤트를 받을 수 있습니다.
  • 대신에 리시버를 해지할 수 없습니다. (리시버 Component의 상태를 disabled로 변경하면 동작하지 않게 만들 수는 있습니다.)

정적 리시버를 등록하는 방법은 다음과 같습니다.

  1. BroadcastReceiver 클래스를 상속한 Receiver 클래스를 구현합니다.
  2. 구현한 클래스를 AndroidManifest.xml에 선언합니다.

리시버 객체 구현

다음과 같이 BroadcastReceiver를 상속받은 클래스를 구현합니다.

class MyReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        Log.d("MyReceiver", "Intent: $intent")
    }
}

Manifest에 리시버 선언

AndroidManifest.xml에 <receiver /> 속성으로 위에서 구현한 리시버를 선언합니다. 중요한 것은 수신받을 인텐트에 대한 정보를 intelt-filter 내부에 선언해야 합니다.

<receiver android:name=".MyReceiver">
    <intent-filter>
        <action android:name="android.intent.action.LOCALE_CHANGED"/>
    </intent-filter>
</receiver>

위의 코드에서 LOCALE_CHANGED 인텐트를 브로드캐스트로 받을 수 있도록 선언했습니다. 이 인텐트는 시스템의 언어가 변경될 때 전달되는 인텐트입니다.

앱을 설치하고 시스템 언어를 변경해보면 다음과 같이 브로드캐스트를 받는 것을 로그로 확인할 수 있습니다.

06-06 21:43:26.097  6607  6607 D MyReceiver: Intent: Intent { act=android.intent.action.LOCALE_CHANGED flg=0x11200010 cmp=com.codechacha.broadcastreceiver/.MyReceiver }

앱에서 정의한 브로드캐스트 받기

android.intent.action~으로 시작하는 Action은 대부분 시스템에서만 보내는 이벤트 입니다. 새로운 Action을 정의하고 내 앱끼리 주고 받을 수도 있습니다.

예를 들어, 다음과 같이 com.codechacha.action.test라는 Action으로 리시버를 정의하면, 내 앱에서 이 Action으로 브로드캐스트를 보냈을 때 받을 수 있습니다.

<receiver android:name=".MyReceiver">
    <intent-filter>
        <action android:name="com.codechacha.action.test"/>
    </intent-filter>
</receiver>

두개 이상의 브로드캐스트 수신

1개의 리시버에서 두개 이상의 Action에 대한 이벤트를 받으려면 다음과 같이 intent-filter에 Action들을 정의하면 됩니다.

<receiver android:name=".MyReceiver">
    <intent-filter>
        <action android:name="android.intent.action.LOCALE_CHANGED"/>
        <action android:name="com.codechacha.action.test"/>
    </intent-filter>
</receiver>

또는 다음과 같이 intent-filter를 두개 정의해도 됩니다.

<receiver android:name=".MyReceiver">
    <intent-filter>
        <action android:name="android.intent.action.LOCALE_CHANGED"/>
    </intent-filter>
    <intent-filter>
        <action android:name="com.codechacha.action.test"/>
    </intent-filter>
</receiver>

동적 BroadcastReceiver 등록

동적 BroadcastReceiver는 앱 실행 중에 코드로 등록되는 리시버를 말합니다. Developer에는 Context-registered receivers 라고 표현합니다.

  • 장점은 원하는 시간에, 원하는 상황에서 리시버를 등록하고 해지할 수 있다는 것입니다.
  • 단점은 앱이 실행되지 않으면 리시버를 등록할 수 없다는 것입니다.

동적 리시버를 등록 및 해지하는 방법은 다음과 같습니다.

  1. 먼저 리시버 객체를 생성합니다.
  2. registerReceiver() 메소드로 리시버를 등록합니다.
  3. unregisterReceiver() 메소드로 리시버를 해지합니다.

리시버 객체 생성

다음과 같이 익명 클래스로 BroadcastReceiver를 상속받는 클래스 객체를 만들 수 있습니다.

private val br: BroadcastReceiver = object:BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Log.d("Receiver", "Intent: $intent")
    }
}

또는, 다음과 같이 클래스를 정의하고 그 클래스를 객체로 생성하는 방법도 있습니다.

private val br = MyBroadcastReceiver()

class MyBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Log.d("MyBroadcastReceiver", "Intent: $intent")
    }
}

리시버 객체를 만드는 방법은 중요하지 않습니다. 편한 방법을 사용하면 됩니다.

리시버 등록 및 해지

다음과 같이 registerReceiver()로 리시버를 등록할 수 있습니다. 인자로 리시버 객체와, IntentFilter 객체를 전달해줘야 합니다. IntentFilter는 브로드캐스트로 받고 싶은 인텐트 정보가 정의되어 있습니다.

private val br = MyBroadcastReceiver()

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    val filter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
    registerReceiver(br, filter)
}

반대로 unregisterReceiver()로 등록된 리시버를 해지할 수 있습니다. 인자로 해지하려는 리시버 객체를 전달해야 합니다.

override fun onDestroy() {
    // ...
    unregisterReceiver(br)
}

Context(Activity) 기반으로 리시버를 등록하는 것이기 때문에, Context(Activity)가 소멸되면 리시버도 해지됩니다.

따라서 register와 unregister는 다음 시점에 해주는 것이 좋습니다.

  • onCreate()에서 리시버를 등록하면 onDestroy()에서 해지하는 것이 좋습니다.
  • onResume()에서 리시버를 등록하면 onPause()에서 해지하는 것이 좋습니다.

동적으로 리시버를 등록하고 언어를 변경하면 다음과 같이 브로드캐스트를 수신하는 것을 로그로 확인할 수 있습니다.

06-06 22:36:43.676  3732  3732 D Receiver: Intent: Intent { act=android.intent.action.LOCALE_CHANGED flg=0x11200010 }

Receiver로 브로드캐스트 전달

브로드캐스트를 전달할 때는 두가지 방법이 있습니다.

  • sendBroadcast(Intent): 리시버들에게 순서 없이 브로드캐스트를 전달합니다. 리시버들에게 거의 동시에 전달되기 때문에 전달 속도가 빠릅니다.
  • sendOrderedBroadcast(Intent, String): 한번에 하나의 리시버에게 브로드캐스트를 전달합니다. IntentFilter의 android:priority의 값을 참고하여 순서를 정하고 이 순서대로 전달합니다. 이전에 보낸 리시버가 모든 처리를 끝내야 다음 리시버로 이벤트를 전달하기 때문에 전달 속도가 느립니다. 이전에 처리한 리시버의 결과를 읽을 수 있으며, 다음 리시버로 전달이 안되도록 막을 수 있습니다.

단순이 이벤트를 전달하는 것이 목적이라면 sendBroadcast()를 사용하시면 됩니다. 이 메소드들은 Activity(Context)에 구현되어있습니다.

암시적인 방법으로 브로드캐스트 전달

암시적인 방법이란 의미는 Framework이 인텐트와 관련된 모든 리시버를 찾고 그 리시버들에게 모두 전달해주는 것을 의미합니다.

아래와 같이 전달할 인텐트를 생성하고 sendBroadcast()의 인자로 전달하면 이 인텐트를 수신하는 모든 리시버에게 전달됩니다.

val intent = Intent("com.codechacha.action.test")
sendBroadcast(intent)

명시적인 방법으로 브로드캐스트 전달

명시적인 방법이란 의미는 브로드캐스트를 전달하는 앱에서 지정한 앱의 리시버에게만 이벤트를 전달하는 것을 의미합니다.

다음과 같이 Intent에 보내고 싶은 앱의 package name을 설정하면 그 앱의 리시버에게만 전달됩니다.

val intent = Intent("com.codechacha.action.test")
intent.setPackage("com.codechacha.broadcastreceiver")
sendBroadcast(intent)

암시적(Implicit) 브로드캐스트 제한 정책

암시적인 방법으로 브로드캐스트를 보내면 앱에 등록된 모든 리시버들에게 전달됩니다. 그 리시버의 앱들이 실행 중이 아니라면 앱 프로세스를 실행시켜 전달합니다. 이런 이유로 시스템이 느려지거나 버벅일 수 있습니다.

이런 문제들을 해결하기 위해 Android는 Target SDK API가 26 이상인 앱을 대상으로, Manifest에 등록된 리시버들은 암시적으로 브로드캐스트를 못받도록 하였습니다.

다시 말하면, registerReceiver()으로 등록한 리시버는 암시적인 브로드캐스트를 받을 수 있지만 Manifest에 등록된 리시버는 이벤트를 받을 수 없습니다. Target SDK API가 26이상인 앱들을 대상으로 하기 때문에, Target을 25이하로 낮춘다면 이 정책을 회피하여 암시적인 브로드캐스트를 받을 수 있습니다.

다행인 점은 ACTION_BOOT_COMPLETED와 같은 일부 브로드캐스트는 예외를 두어 Manifeste에 등록된 리시버도 받을 수 있도록 하였습니다. Implicit Broadcast Exceptions를 참고하시면 어떤 Action들이 예외인지 알 수 있습니다.

Receiver에서 오래 걸리는 작업 처리

리시버에서 이벤트를 받으면 onReceive()가 호출됩니다. 짧게 걸리는 일은 그냥 처리하면 되지만 오래 걸리는 작업은 다른 쓰레드에서 처리하거나 Scheduler로 작업을 등록하여 처리해야 합니다.

override fun onReceive(context: Context?, intent: Intent?) {
    ...
}

registerReceiver()으로 리시버를 등록할 때 Handler를 인자로 전달하여 그 Handler에서 onReceive() 처리되도록 할 수 있습니다. 하지만 Manifest에 등록된 리시버는 Handler를 선택할 수 없고, 앱의 Main handler에서 처리됩니다. 작업이 길어질 경우 Main thread에서 처리되는 다른 작업에 영향을 줄 수 있습니다.

이런 이유로, 다음과 같이 Async 객체를 생성하고 작업을 위임하여 onReceive()가 빠르게 처리되도록 만들 수 있습니다.

class MyBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val pendingResult: PendingResult = goAsync()
        val asyncTask = Task(pendingResult, intent)
        asyncTask.execute()
    }

    private class Task(
            private val pendingResult: PendingResult,
            private val intent: Intent
    ) : AsyncTask<String, Int, String>() {

        override fun doInBackground(vararg params: String?): String {
            val sb = StringBuilder()
            sb.append("Action: ${intent.action}\n")
            sb.append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n")
            return toString().also { log ->
                Log.d(TAG, log)
            }
        }

        override fun onPostExecute(result: String?) {
            super.onPostExecute(result)
            // Must call finish() so the BroadcastReceiver can be recycled.
            pendingResult.finish()
        }
    }

위의 예제에서 goAsync()pendingResult.finish()등의 코드를 사용하는 이유는, 다른 쓰레드에서 처리 중인 작업이 진행 중이라는 것을 시스템에게 알려줘 실행 중인 프로세스를 종료하지 않게 만들기 위해서입니다. 시스템은 자원이 부족한 경우, 미사용 중인 프로세스를 종료하여 메모리를 회수할 수 있기 때문에 이런 방법으로 프로세스가 종료되지 않도록 해줘야 합니다.

권한으로 Broadcast 발송/수신 제한

자신의 앱끼리 주고 받으려고 action을 정의하였는데, 어떤 악성 앱이 이것을 알고 동일한 action을 브로드캐스트할 수 있습니다. 권한을 보내거나 받을 때 퍼미션을 설정하여 이런 일을 방지할 수 있습니다.

권한을 갖고 있는 앱으로부터 브로드캐스트 받기

다음과 같이 리시버를 선언할 때 퍼미션을 설정하면 그 퍼미션을 갖고 있는 앱만 브로드캐스트를 보낼 수 있습니다. 만약 권한이 없다면 브로드캐스트를 보낼 수 없게 됩니다.

<receiver android:name=".MyReceiver"
        android:permission="android.permission.SEND_SMS">
    <intent-filter>
        <action android:name="com.codechacha.action.test"/>
    </intent-filter>
</receiver>

위의 예제에서는 com.codechacha.action.test 인텐트를 보내는 앱이 SEND_SMS 권한을 갖고 있지 않다면, 이 리시버는 브로드캐스트를 받지 않습니다.

권한을 갖고 있는 리시버에게만 브로드캐스트 전달

다음과 같이 sendBroadcast()의 인자에 퍼미션을 전달하면, 이 퍼미션을 갖고 있는 앱의 리시버만 브로드캐스트를 받을 수 있습니다.

Intent intent = new Intent("com.codechacha.action.test");
intent.setPackage("com.codechacha.broadcastreceiver");
sendBroadcast(intent, Manifest.permission.SEND_SMS);

위의 예제에서는 com.codechacha.broadcastreceiver 앱이 SEND_SMS 권한을 갖고 있지 않다면 브로드캐스트를 받을 수 없습니다.

참고

댓글을 보거나 쓰려면 이 버튼을 눌러주세요.
codechachaCopyright ©2019 codechacha