브로드캐스트 리시버하면 안드로이드에서 빠질 수 없는 4대 컴포넌트 중에 하나다. 근데 우린 이에 대해 잘 알고 있을까? 조금 더 자세하게 알아보고 정리하고자 포스팅을 한다
브로드캐스트 리시버는 옵저버(Observer) 패턴을 안드로이드에서 구현한 방식이다. 어디선가 특별한 이벤트가 발생할 때, 이벤트를 기다리던 쪽에서 해야할 작업이 있다.
이를 온라인 서점에 책을 등록하는 것에 비유해보자
1) 책이 한 권 출판되었다
2) 출간 사실을 온라인 서점에 연락해서 알린다
3) 각 온라인 서점은 책에 관해 데이터를 입력한다
BroadcastReceiver 입장에서는
1) - Intent
2) - Context에서 sendBroadcast() 메서드 호출
3) - BroadcastReceiver의 onReceive() 메서드
와 대응된다.
BroadcastReceiver는 바로 옵저버이고, 이벤트는 sendBroadcast()에 전달되는 Intent이다.
브로드캐스트는 말 그대로 방송용 이벤트이다. 방송을 청취하는 리시버가 등록되어 있다면 리시버에서 받아서 처리한다. 리시버가 없다면 브로드캐스트는 그저 공허한 방송이 된다.
Context에는 registerReceiver()
와 unregisterReceiver()
메서드가 있는데, 여러 컴포넌트(액티비티, 서비스, Application)에서 사용될 수 있다.
각 컴포넌트는 실행 중인 상태에서 브로드캐스트를 받으려고 할 때 브로드캐스트 리시버를 등록한다.
BroadcastReceiver에는 추상 메서드가 onReceive() 하나뿐이고 이 메서드를 구현하면 된다
abstract void onReceive(Context context, Intent intent)
onReceive() 이외에는 대부분 final 메서드이고 override가 허용되지 않는다. final 메서드들은 onReceive()에서 호출하는 메서드라고 보면 된다.
BroadcastReceiver는 ContentProvider와 마찬가지로 ContextWrapper 하위 클래스가 아니다.
+) 참고용 Context 클래스 구조도 첨부
context란? 애플리케이션 환경에 대한 전역 정보에 접근할 수 있는 연결 장치이자 Android 시스템에서 implements를 제공하는 추상 클래스이다.
그렇지만 Context는 전달되므로 이것으로 startService(), startActivity() 외에 sendBroadcast()를 다시 호출할 수도 있다.
그치만 이벤트가 발생하면 그에 맞게 화면을 보여주거나 백그라운드 작업을 하는 경우가 많아서, 실제로 주로 실행하는 것은 startService나 startActivity이다.
특정 이벤트가 발생할 때 -부팅이 완료되거나 언어 설정이 변경되는 경우- startActivity()나 startService()를 직접 실행하는 방법은 없다.
sendBroadcast()를 통해서 브로드캐스트가 전달되고, 이 때 화면을 띄우려면 BroadcastReceiver의 onReceive() 에서 startActivity()를 실행한다. 혹은 UI가 없는 내부 작업이 필요하다면 startService()를 실행한다.
메신저 앱의 경우 누군가 내게 메세지를 보내면 푸시 메시지를 BroadcastReceiver
에서 받는다. 그 순간에 띵동! 소리가 나고 화면이 켜지면서 팝업에 대화 내용이 뜨는데, 이 팝업이 바로 onReceive()에서 startActivity()를 실행한 결과이다.
물론, startActivity()와 startService()가 둘 다 실행되는 경우도 있다. 배경 음악이 깔리면서 화면이 떠야한다면 그럴 수 있다.
이 메서드는 메인 스레드에서 실행되므로 시간 제한이 있다.
내에 실행을 마쳐야 ANR이 발생하지 않는다.
메인스레드에서 실행되므로 ANR 타이암웃인 1분/10초/5초도 아닌 훨씬 짧은 시간 내에 처리가 완료되어야 한다. 왜냐하면 UI에서 이벤트 처리가 늦어지는 원인이 될 수 있기 때문이다.
단일 이벤트에 대해서 하나의 앱에 여러 브로드캐스트 리시버가 등록된 경우도 주의해야 한다.
왜냐하면 여러 브로드캐스트 리시버가 등록되어 있다면 여러 브로드캐스트 리시버의 onReceive()가 순차적으로 하나씩 실행되기 때문에 브로드캐스트 리시버를 실행하느라 UI 동작에 문제가 생길 수 있다.
Q) 브로드캐스트 리시버에서 Toast를 띄우면 잘 동작할까?
Toast는 비동기 동작이다. 앱에 포그라운드 프로세스라면 Toast는 정상동작한다. 하지만 백그라운드 프로세스이거나 앱에서 실행 중인 컴포넌트가 브로드캐스트 리시버밖에 없다면, onReceive() 메서드가 끝나자마자 프로세스 우선순위에 밀려서 프로세스가 종료될 수도 있다. 이 때는 토스트가 뜨지 못한다.
그러므로 해당 메서드에서 Toast는 동작할 수도 있고 아닐 수도 있다. 동작을 백퍼센트 보장해주지 못하기 때문에 이 메서드에서 Toast를 쓰는게 맞는지 다시 검토해보자
onReceive에 Context가 전달되지만, Context의 메서드인 registerReceive()나 bindService()를 호출하면 런타임 exception을 발생시킨다.
onReceive에 전달된 Context는 구체적으로 ContextWrapper인 Application을 다시 감싼 ReceiverRestrictedContext
인스턴스이다.
ReceiverRestrictedContext
는 ContextImpl의 내부 클래스이면서 ContextWrapper를 상속하고 registerReceive()나 bindService()를 오버라이드해서 예외를 발생시키게 한 것이다.
서비스가 이미 실행중이라면 브로드캐스트 리시버에서 bindService()가 아닌 peekService() 메서드를 호출하여 서비스에 접근한 뒤, 서비스의 메서드를 실행 할 수 있다.
+) 개인적인 궁금함을 탐구해봤다. 관심 없으신 분들은 바로 뒤 파트인 리시버 등록이야기로 넘어가셔도 된다.
난 이 ReceiverRestrictedContext
의 내부 코드가 실제로도 그런지 궁금했고 Impl의 코드를 확인했더니 아래와 같았다
class ReceiverRestrictedContext extends ContextWrapper {
@UnsupportedAppUsage
ReceiverRestrictedContext(Context base) {
super(base);
}
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
return registerReceiver(receiver, filter, null, null);
}
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
String broadcastPermission, Handler scheduler) {
if (receiver == null) {
// Allow retrieving current sticky broadcast; this is safe since we
// aren't actually registering a receiver.
return super.registerReceiver(null, filter, broadcastPermission, scheduler);
} else {
throw new ReceiverCallNotAllowedException(
"BroadcastReceiver components are not allowed to register to receive intents");
}
}
@Override
public Intent registerReceiverForAllUsers(BroadcastReceiver receiver, IntentFilter filter,
String broadcastPermission, Handler scheduler) {
return registerReceiverAsUser(
receiver, UserHandle.ALL, filter, broadcastPermission, scheduler);
}
@Override
public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
IntentFilter filter, String broadcastPermission, Handler scheduler) {
if (receiver == null) {
// Allow retrieving current sticky broadcast; this is safe since we
// aren't actually registering a receiver.
return super.registerReceiverAsUser(null, user, filter, broadcastPermission, scheduler);
} else {
throw new ReceiverCallNotAllowedException(
"BroadcastReceiver components are not allowed to register to receive intents");
}
}
@Override
public boolean bindService(Intent service, ServiceConnection conn, int flags) {
throw new ReceiverCallNotAllowedException(
"BroadcastReceiver components are not allowed to bind to services");
}
@Override
public boolean bindService(
Intent service, int flags, Executor executor, ServiceConnection conn) {
throw new ReceiverCallNotAllowedException(
"BroadcastReceiver components are not allowed to bind to services");
}
@Override
public boolean bindIsolatedService(Intent service, int flags, String instanceName,
Executor executor, ServiceConnection conn) {
throw new ReceiverCallNotAllowedException(
"BroadcastReceiver components are not allowed to bind to services");
}
}
그외에도 리서치를 하는데 흥미로운 stackoverflow 글을 발견했고 흥미진진한 답변을 발견했다
What is the context passed by android in the onReceive() of BroadcastReceiver
그냥 context를 넘기면 ReceiverRestrictedContext
가 넘어가는데, context.getApplicationContext()
로 하면 실제 applictioncontext가 넘어간다거나 최신 안드로이드 코드엔 ReceiverRestrictedContext
가 사라졌다거나 하는 답변들이 있다
전자는 그렇다쳐도 후자가 신뢰성이 없어서 직접 코드를 짜서 확인해보았다.
그러나 동적 리시버로 등록해서 했더니 계속 onReceive:: com.woga.mailto.MainActivity@35486ef
라는 액티비티 이름만 나와서 context가 실제로 ReceiverRestrictedContext인지는 확인을 못해봤다!
다른 블로그를 봤을 땐
동적으로 하면 로그캣에 context가 activity로 display되고 정적 리시버로 등록하면 위 캡쳐본속처럼 display된다고 한다.
정적 리시버는 오레오 이상부터 몇몇개를 제외하고 안드로이드가 제한을 두고 있어서 구현하기 어려운듯 했다
출처 : https://developer.android.com/guide/components/broadcast-exceptions
리시버리스트릭컨텍스트가 저 스택오버플로우 답변처럼 사라진거는 아닌 듯 하다. 정적 리시버가 제한이 되면서 쓰이는 상황이 달라져서 저렇게 답변한게 아닌가 추측해본다. 다른 방법으로 구현 후 확인하게 된다면 마저 캡쳐본을 첨부하도록 하겠다!
이미 앞에서 개인적인 궁금함으로 알아보면서 구구절절 이야기를 했지만, 리시버 등록에는 2가지가 있다.
정적으로 등록된 브로드캐스트 리시버는 앱을 설치하자마자 사용이 가능하다.
정적인 등록은 바로 AndroidManifest.xml
에 브로드캐스트 리시버를 추가하는 것이다. 이 정적으로 등록된 리시버는 브로드캐스트가 발생하면 항상 반응한다.
주로 시스템 이벤트를 받을 때 많이 사용하여 앱이 실행중이지 않아도 프로세스가 뜨고서 이벤트를 처리한다.
<receiver android:name=".os.SmsMessageReceiver">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
SMS_RECEIVED_ACTION은 아직 정적 리시버를 지원한다.
외부 프로세스의 이벤트를 받는 브로드캐스트 리시버를 만들 때가 많기 때문에 샘플처럼 intent-filter를 추가해서 암시적 인텐트를 전달받는다. 하지만 로컬 프로세스에서만 사용하는 경우에는 이를 넣지 않고 명시적 인텐트를 전달 받을 수 도 있다.
Intent API 문서에 보면 시스템의 브로드 캐스트 액션이 상수로 정의돼 있다. 이걸 하나하나 Broadcast Action:
쳐서 긁기에는 시간이 오래 걸릴 듯 하여 누군가가 정리한 표에서 가지고 왔다.
ACTION_AIRPLANE_MODE_CHANGED
ACTION_BATTERY_CHANGED
ACTION_BATTERY_LOW
ACTION_BATTERY_OKAY
ACTION_BOOT_COMPLETED
ACTION_CAMERA_BUTTON
ACTION_CLOSE_SYSTEM_DIALOGS
ACTION_CONFIGURATION_CHANGED
ACTION_DATE_CHANGED
ACTION_DEVICE_STORAGE_LOW
ACTION_DEVICE_STORAGE_OK
ACTION_DOCK_EVENT
ACTION_GTALK_SERVICE_CONNECTED
ACTION_GTALK_SERVICE_DISCONNECTED
ACTION_HEADSET_PLUG
ACTION_INPUT_METHOD_CHANGED
ACTION_LOCALE_CHANGED
ACTION_MANAGE_PACKAGE_STORAGE
ACTION_MEDIA_BAD_REMOVAL
ACTION_MEDIA_BUTTON
ACTION_MEDIA_CHECKING
ACTION_MEDIA_EJECT
ACTION_MEDIA_MOUNTED
ACTION_MEDIA_NOFS
ACTION_MEDIA_REMOVED
ACTION_MEDIA_SCANNER_FINISHED
ACTION_MEDIA_SCANNER_SCAN_FILE
ACTION_MEDIA_SCANNER_STARTED
ACTION_MEDIA_SHARED
ACTION_MEDIA_UNMOUNTABLE
ACTION_MEDIA_UNMOUNTED
ACTION_NEW_OUTGOING_CALL
ACTION_PACKAGE_ADDED
ACTION_PACKAGE_CHANGED
ACTION_PACKAGE_DATA_CLEARED
ACTION_PACKAGE_INSTALL
ACTION_PACKAGE_REMOVED
ACTION_PACKAGE_REPLACED
ACTION_PACKAGE_RESTARTED
ACTION_POWER_CONNECTED
ACTION_POWER_DISCONNECTED
ACTION_PROVIER_CHANGED
ACTION_REBOOT
ACTION_SCREEN_OFF
ACTION_SCREEN_ON
ACTION_SHUTDOWN
ACTION_TIMZONE_CHANGED
ACTION_TIME_CHANGED
ACTION_TIME_TICK
ACTION_UID_REMOVED
ACTION_UMS_CONNECTED
ACTION_UMS_DISCONNECTED
ACTION_USER_PRESENT
ACTION_WALLPAPER_CHANGED
총 70개라..
문서를 보면 This is a protected intent that can only be sent by the system
라는 메시지가 많이 나온다. 즉 해당 인텐트는 시스템에서만 발생시킬 수 있고 앱에서는 발생시킬 수 없다.
그 중 하나인 아래 시스템 인텐트를 브로드캐스트해보았다.
/**
* Broadcast Action: External power has been connected to the device.
* This is intended for applications that wish to register specifically to this notification.
* Unlike ACTION_BATTERY_CHANGED, applications will be woken for this and so do not have to
* stay active to receive this notification. This action can be used to implement actions
* that wait until power is available to trigger.
*
* <p class="note">This is a protected intent that can only be sent
* by the system.
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_POWER_CONNECTED = "android.intent.action.ACTION_POWER_CONNECTED";
이런 시스템 인텐트를 브로드캐스트하면 Security Exception
을 만나게 된다.
참고로 Intent 클래스에 정의된 브로드캐스트 액션이 모두 시스템 이벤트인 것은 아니다
ACTION_MEDIA_SCANNR_SCAN_FILE
액션 같은 것은 앱에서 발생시키라고 있는 것이다.
이미지를 저장했는데 미디어 스캐닝이 안 돼서 곧바로 화면에 가져올 수 없는 경우 앱에서 이 액션을 브로드캐스트해서 미디어 스캐너가 동작하게 하는 것이다.
Context의 registerReceiver() 메서드로 브로드캐스트 리시버를 동적으로 등록한다. 이는 앱 프로세스가 떠 있고 브로드캐스트 리시버를 등록한 활성화된 컴포넌트가 있을 때만 동작하는 것이다.
브로드캐스트 리시버는 unregisterReceiver() 메서드에서 해제한다.
일반적으로 Activity에서는 포그라운드 라이프타임인 onResume/onPause에서 registerReceiver()/unregisterReceiver()를 호출한다.
볼륨 변경 시 브로드캐스트 처리
val receiver = object : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
if (intent.action == VOLUME_CHANGED_ACTION) {
val value = intent.getIntExtra("android.media.EXTRA_VOLUME_STREAM_VALUE", -1)
if (value > -1) {
mVolumeSeekBar.setProgress(value)
}
}
}
}
override fun onResume() {
super.onResume()
val filter = IntentFilter()
filter.addAction(VOLUME_CHANGED_ACTION)
registerReceiver(receiver, filter)
}
override fun onPause() {
unregisterReceiver(receiver)
super.onPause()
}
VOLUME_CHANGED_ACTION은 api 문서에 없는 내용이다. 코드상으로는
AudioManager
클래스에 상수로 있는데@hide
애너테이션으로 숨겨져 있다. 이 샘플과 같이 액션명을 알 수 있다면 처리할 수 있는 케이스가 많다.
바탕화면에 숏컷 설치하는 것도 브로드캐스트를 사용할수도 있었다.
com.android.launcher.action.INSTALL_SHORTCUT
도 API 문서엔 없는 내용이다.
아래 예시처럼 퍼미션과 인텐트로 브로드캐스트 사용해서 숏컷 설치를 할 수 있지만,
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" />
<application>
<receiver
android:name=".InstallShortcutReceiver"
android:permission="com.android.launcher.permission.INSTALL_SHORTCUT">
<intent-filter>
<action android:name="com.android.launcher.action.INSTALL_SHORTCUT" />
//+) 구시대 코드를 찾아보니 이런 action도 있다
//<action android:name="general.intent.action.SHORTCUT_ADDED"/>
</intent-filter>
</receiver>
</application>
알다시피 AndroidManifest에 지정하는 건 오레오 이후 지원하지 않는다.
API 25 이상은 ShortcutManager
라는 클래스를 통해 생성할 수 있다. 자세한 내용은 아래 공식 문서를 확인하자.
https://developer.android.com/develop/ui/views/launch/shortcuts/creating-shortcuts
그저 적당하게 알아보자 하고 시작했던 파트고 다 읽고 나서 정리하는데 시간이 꽤나 오래걸렸다. 브로드캐스트를 잘 쓰지 않기도해서 개념으로만 알고 있던 것을 조금 더 파고드는 시간을 가졌던 거 같아 뿌듯하다
참고로 책 안드로이드 NextStep과 여러 리서치를 통해 정리했다!