안드로이드 앱은 Android System
, System App
, Thrid Party App
으로부터 이벤트를 받을 수 있다.
우선 Android System
이란 핸드폰에 기본으로 설치된 앱이 아닌, 시스템 자체에서 보내는 브로드캐스트를 의미한다. 대표적으로 비행기 모드 상태, 네트워크 연결 상태, wifi 연결 여부, 셀룰러 데이터 연결 여부 등이 있다.
System App
이란 우리가 핸드폰을 살 때, 기본적으로 설치된 앱을 의미한다. 이 앱에서 또한 우리가 만든 앱에 브로드캐스트를 전송할 수 있다. 대표적으로 문자메시지를 수신받았을 때, 전화가 왔을 때 등이 있다.
마지막으로 개발자가 직접 만든 앱인 Third Party App
에선 개발자가 원하는 이벤트를 전송할 수 있다. 가령, A라는 회사에서 사내 시스템 앱으로 캘린더 앱과 업무용 앱을 만들었다 치자. 이때, 캘린더 앱 일정이 등록되었을 때, 업무용 앱이 브로드캐스트를 받는 경우가 있다.
브로드캐스트 구현을 위해 우선 해야할 일은 BroadcastReceiver
의 구현 클래스를 만드는 것이다. 이 클래스를 오버라이딩 했을 시, 내부적으로 onReceive(context Context, intent Intent) {...}
콜백 메서드를 작성할 수 있다. 이 곳이야말로 내 앱이 브로드캐스트를 받았을 때 호출되는 곳이다.
onReceive()
메서드의 실행 방식의 이해에 따른 주의점이 있다. 안드로이드 시스템은 메모리 최적화를 위해 onReceive()
메서드 실행이 끝까지 진행되었을 때 해당 객체를 메모리에서 삭제하려 한다. 따라서 onReceive()
메서드에서 장기적인 백그라운드 작업을 진행시켰을 때, ANR에러 및 앱의 비정상종료가 발생한다. 물론, onReceive()
는 메인 스레드에서 동작한다. 이런 이유로 안드로이드 공식 홈페이지에 의거, 아래 방식을 통해 비동기 작업을 진행해야한다.
[onReceive에서 비동기작업 권장사항]
1.goAsync()
메서드 호출 및 비동기 작업 실행함을 알림.
2. 무거운 작업 실행 시,Service
/WorkManager
/JobScheduler
/Coroutine
의 사용 고려.
3. 작업 완료 시,goAsync()
의 반환타입인PendingResult
타입의.finish()
호출로 비동기 작업이 끝났음을 알림
아래는 안드로이드 공식 홈페이지에 있는 BroadcastReceiver
의 구현클래스 작성 예시 코드이다.
class MyBroadcastReceiver : BroadcastReceiver() {
@Inject
lateinit var dataRepository: DataRepository
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == "com.example.snippets.ACTION_UPDATE_DATA") {
val data = intent.getStringExtra("com.example.snippets.DATA") ?: "No data"
// Do something with the data, for example send it to a data repository:
dataRepository.updateData(data)
}
}
}
앱을 처음 설치했을 때, 브로드캐스트 리시버를 등록할 것인가? 아니면 안드로이드 컴포넌트 수명주기에 맞춰 리시버를 등록할 것인가? 를 경정해야 한다.
[리시버 등록 법]
Manifest.xml
에 등록Android Component
에 등록
'전자'의 경우, Manifest.xml
을 통해 등록할 수 있다. 이는 Manifest.xml
에 리시버가 등록된 앱은 해당 앱 설치 시, 안드로이드 시스템이 브로드캐스트를 등록시킨다. 그럼으로써 Android System
/Thrid Party App
/System App
에서 발생한 브로드캐스트를 해당 앱에도 전송시켜주게 된다. 하지만 이 방식은 문제가 존재한다. 만약 A라는 사람의 핸드폰에 10개의 앱이 있고, 이들은 동일한 시스템 브로드캐스트를 Manifest.xml
을 통해 등록되었다고 가정해보자. 이럴 경우, A라는 사람의 핸드폰에서 브로드캐스트가 발생할 때마다 10개의 앱에 브로드캐스트가 전송하게 되고 해당 앱들이 실행되므로 배터리를 많이 사용하게 된다. 따라서 안드로이드 공식 홈페이지는 이보단 '후자'의 방법을 권장한다.
'후자'의 경우, Activity
/Service
에서 등록할 수 있다. 이 컴포넌트들은 생명주기가 존재하므로, 개발자가 원하는 타이밍에 리시버를 등록하고 해제하는것이 가능하다. 다만, 이 방식은 메모리 누수가 발생할 수 있는데, 이를 방지하기 위해 개발자는 unregisterReceiver
만 신경써서 잘 호출해주면 된다. 아래는 예시 코드이다.
// Compose의 생명주기에 따라 리시버 등록/해제하기
@Composable
fun MyStatefulScreen() {
val myBroadcastReceiver = remember { MyBroadcastReceiver() }
val context = LocalContext.current
LifecycleStartEffect(true) {
// ...
ContextCompat.registerReceiver(context, myBroadcastReceiver, filter, flags)
onStopOrDispose { context.unregisterReceiver(myBroadcastReceiver) }
}
MyStatelessScreen()
}
@Composable
fun MyStatelessScreen() {
// Implement your screen
}
// Activity의 생명주기에 따라 리시버 등록/해제하기
class MyActivity : ComponentActivity() {
private val myBroadcastReceiver = MyBroadcastReceiver()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
ContextCompat.registerReceiver(this, myBroadcastReceiver, filter, receiverFlags)
setContent { MyApp() }
}
override fun onDestroy() {
super.onDestroy()
// When you forget to unregister your receiver here, you're causing a leak!
this.unregisterReceiver(myBroadcastReceiver)
}
}
내가 등록한 리시버를 어느 범위까지 공개할지 설정할 수 있다. 2가지 방식이 있는데, 한 가지는 exported
설정이고, 또 한가지는 permission
설정이다.
[보안 범위 설정 법]
- exported
- permission
Android System
/System App
/Third Party App
이 브로드캐스트를 보낸다 한들, 위 설정값에 따라 브로드캐스트 전송을 막을 수 있다. 이 방식을 고려해야하는 이유는 악의적인 해커가 적절치 않은 브로드캐스트를 전송했을 경우, 비용이 발생하기 때문이다. 대표적으로 브로드캐스트 수신 시, API호출을 한다고 가정해보자. 이때 해커가 브로드캐스트의 악의적 전송을로 서버의 무한정 API호출 및 공격을 시도할 수 도 있다.
따라서 위 방식들로 이를 방어할 필요가 있다. 우선 exported
속성이란, System App
/Third Party App
이 전송하는 브로드캐스트를 받지 않겠다는 선언이다. 즉, 어떠한 App
에게도 내가 가진 리시버를 노출하지 않겠다는 선언이다.
그 다음 permission
이란, 특정 브로드캐스트 실행을 위해, 송신자와 수신자 모두 권한을 허용해야함을 의미한다. 예를 들어, 문자 메시지 브로드캐스트가 있다 가정해보자. 그럼 우선, 송/수신자는 해당 권한을 Manifest.xml
에 명시하고 사용자로부터 허용받아야만 한다.
<uses-permission android:name="android.permission.READ_SMS" />
송신자 앱도 권한을 받았으니 수신자 앱도 권한을 받아야한다. 그러기 위해 해당 앱의 리시버 선언 시, 아래와 같은 코드 작성이 이뤄져야 한다.
[Manifest에서 리시버 등록]
<receiver
android:name=".util.MySMSReceiver"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
[Component에서 리시버 등록]
val smsReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "android.provider.Telephony.SMS_RECEIVED") {
println("SMS received!")
}
}
}
fun registerSmsReceiver(context: Context) {
val intentFilter = IntentFilter("android.provider.Telephony.SMS_RECEIVED")
context.registerReceiver(smsReceiver, intentFilter, "android.permission.BROADCAST_SMS", null)
}
fun unregisterSmsReceiver(context: Context) {
// 등록한 리시버를 해제
context.unregisterReceiver(smsReceiver)
}
브로드캐스트 송신자 즉, System App
/Thrid Party App
입장에선 전송할 브로드캐스트 종류와 전송할 앱을 결정한다.
[송신자의 전송 방식]
- 전송할 브로드캐스트 종류
- 전송할 앱
우선 System App
의 경우를 보자. 이 앱들은 안드로이드 기기에 설치되는 모든 앱에게 브로드 캐스트를 전송해야한다. 따라서 setPackage
와 같은 옵션은 따로 설정하지 않는것으로 보인다. 하지만 Thrid Party App
의 경우, 하나의 회사에서 만들어진 업무 앱들끼리만 브로드캐스트를 전송하기 위해 setPackage
를 설정할 수 있다.
즉, 전송한 브로드캐스트의 종류는 System App
이든, Thrid Party App
이든간에 무조건적으로 설정한다. 예를 들어, System App
에 해당하는 문자메시지 앱의 경우, android.provider.Telephony.SMS_RECEIVED
를 전송함으로써 수신받은 문자메시지를 모든 앱들에게 전송한다. (setPackage
없이) 반면, Thrid Party App
의 경우, kr.co.helloApp.CUSTOM_BROADCAST
와 같은 이벤트를 전송할 수 있다.