본문 바로가기

Android(Kotlin) Study A부터 Z까지 등반

Broad Cast 이론 ( Android 공식문서 )

BroadCast Overview

Publisher - Subscribe Pattern과 유사함.

💡 Pub가 이벤트를 생성할 때마다 굳이 알려주진 않는다. 하나의 중간 수신자가 알려준다.

 💡 Pub가 기기, Sub가 앱이라고 할 수 있을듯하다.

 

💡 해당 패턴에 대해서 어떤 구조인지만 봐도 브로드캐스트 리시버가 어떤 역할을 해주는지 알 수 있다.

 

BroadCast Receiver란?

Android 시스템에서 관심 있는 이벤트가 발생할 때 브로드캐스트를 전송할 수 있다. 이를 감지하고 그 리시버가 있는 앱으로 전달해주는 역할을 한다. 기기와 앱에 대한 메세징 시스템이라고 생각하면 될 것 같다.

💡 백그라운드에서 많이 돌려버리면 메모리 초과가 생기고 배터리가 많이 닳게 된다. 특히 암시적 브로드 캐스트는 수신 대기 하는 백그라운드 프로세스를 시작하기 때문에 조심해서 써야한다.

 

뭐로 소통을 하는가?

이벤트는 인텐트 필터에 등록한 액션들에서 맞는 것이 있다면 받아올 수 있다.

인텐트 필터를 등록하는 방법은 2가지이다. 매니페스트 선언(정적), 컨텍스트 등록(동적)

매니페스트 선언 Receiver (정적)

매니페스트에서 브로드캐스트 수신기를 선언하면 브로드캐스트가 전송될 때 시스템이 앱을 시작합니다(앱이 아직 실행되지 않은 경우).

<receiver android:name=".MyBroadcastReceiver"  android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
    </intent-filter>
</receiver>

정적 리시버를 등록하면 구현한 앱이 동작 중이 아닌 상황에서도 리시버가 동작하게 된다. 그래서 무의미한 CPU 낭비를 고려해서 써야한다.

이걸 어떻게 받아내는가? 서브클래스 구현하여 onReceive에서 받을 수 있다.

private const val TAG = "MyBroadcastReceiver"
class MyBroadcastReceiver : BroadcastReceiver() {

   override fun onReceive(context: Context, intent: Intent) {
       StringBuilder().apply {
           append("Action: ${intent.action}\\n")
           append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\\n")
           toString().also { log ->
               Log.d(TAG, log)
               Toast.makeText(context, log, Toast.LENGTH_LONG).show()
           }
       }
   }
}

소스에서 선언 동적, 명시적 정확히는 컨텍스트에 등록

Receiver 를 원하는 시점에 등록(registerReceiver) 과 해제(unregisterReceiver) 하여 자유롭게 사용한다.

  • 메니페스트에 등록하지않고 소스상에 등록을 한다.
  • 자신이 등록한 component 의 생명 주기가 끝나면 사라진다.

(Context 가 유효할 동안 동작한다.)

클래스를 액티비티에서(context) 등록하는 법(매니페스트 선언 없이)

val br: BroadcastReceiver = MyBroadcastReceiver() //인스턴스 생성

    val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply {
        addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED)
    }
    registerReceiver(br, filter)

컨텍스트에 등록된 수신자는 등록 컨텍스트가 유효한 동안 브로드캐스트를 수신합니다. 예를 들어 [Activity](<https://developer.android.com/reference/android/app/Activity?hl=ko>) 컨텍스트 내에 등록하면 활동이 제거되지 않는 한 브로드캐스트를 수신합니다. Application 컨텍스트에 등록하면 앱이 실행되는 동안 브로드캐스트를 수신합니다.

💡 따라서 액티비티가 종료시에도 수신 받으려면 Application Context로 보내면 되겠쥬

 

수신을 중지하려면 unregisterReceiver를 쓰면 된다. 하지만 고려해야할 사항이 있다.

 💡 예를 들어 활동의 컨텍스트를 사용하여 [onCreate(Bundle)](<https://developer.android.com/reference/android/app/Activity?hl=ko#onCreate(android.os.Bundle)>)에 수신자를 등록했으면 활동 컨텍스트에서 수신자가 유출되지 않도록 [onDestroy()](<https://developer.android.com/reference/android/app/Activity?hl=ko#onDestroy()>)에서 수신자를 등록 취소해야 합니다. [onResume()](<https://developer.android.com/reference/android/app/Activity?hl=ko#onResume()>)에 수신자를 등록했으면 [onPause()](<https://developer.android.com/reference/android/app/Activity?hl=ko#onPause()>)에서 수신자를 등록 취소하여 수신자가 여러 번 등록되지 않도록 해야 합니다(일시중지되었을 때 브로드캐스트를 수신하지 않으려면). 이렇게 하면 불필요한 시스템 오버헤드를 줄일 수 있습니다. 그리고 [onSaveInstanceState(Bundle)](<https://developer.android.com/reference/android/app/Activity?hl=ko#onSaveInstanceState(android.os.Bundle)>)에서 등록을 취소해서는 안 됩니다. 사용자가 기록 스택으로 되돌아가면 이 메서드가 호출되지 않기 때문입니다.

 

💡 onReceive가 실행 될 때는 포그라운드 프로세스로 간주되지만, 반환되는 순간부터는 Receiver가 활성 상태가 아니게 된다. 그래서 우선순위가 낮은 프로세스로 간주되게 된다. 종료 될수도 있음. —> 그럼 대기도 안하는건가?

 

  • 오래 걸리는건 goAsync이나 JobScheduler를 사용하는 것을 추천한다.

goAsync 사용 예제 onReceive작업이 오래 걸릴 때 유용하다.

    private const val TAG = "MyBroadcastReceiver"

    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()
            }
        }
    }

인텐트 필터로 브로드캐스트의 리시버 조건을 명시, 암시해줬다. 암시적, 명시적 인텐트란 무엇일까?

인텐트의 종류

 💡 간단하게 말했을 때 암시적 : 의도가 불명확하다. 해당 액션에 대해서 정확히 실행할 액티비티에 대한 보장이 없다. 액션 정보를 가지고 있음.(이런 액션을 취할거니깐 너가 정해줘) 명시적 : 의도가 명확하다. 어디 클래스로 보낼지 지정되어있음. 액시비티 전환 등

 

암시적

val intent = Intent(Intent.ACTION_DIAL) //ACTION_DIAL 액션이 있는 앱과 Uri가 tel:로 가능한 액티비티를 찾아서 실행을 해줘야함,
val TEST_DIAL_NUMBER = Uri.fromParts("tel", "5551212", null)
intent.setData(TEST_DIAL_NUMBER)
startActivity(intent)

암시적 인텐트의 액션 종류

명시적

val intent = Intent(this, SubActivity::class.java)
startActivity(intent) //인텐트를 수행할 class를 명시적으로 지정

💡 ActivityManager가 PackageManager에 resolve 액티비티 API를 호출하여 적합한 액티비티가 어떤건지 물어본다. 이과정을 Resolving이라고함 여기서의 Resolving이 모든 곳에 통용되는 의미는 아닌듯하다!

 

특정 기능들을 사용할 때 필요한 권한들이 있다. 우린 이것을 위해 사용자에게 권한 요청을 해야하며, Permission이라고 한다.

퍼미션도 protectionLevel이라는 것이 있다. Level에 따라 명시적으로 승인을 받을지 안 받을지가 정해져있다. Dangerouse는 항상 앱 깔고 권한 승인으로 뜨는 것들이다(전화, 위치 등등 승인)

암시적 인텐트 실행과정

암시적 인텐트 장점

 💡 PDF 파일을 열거다. 근데 PDF를 읽을 수 있는 앱은 Chrome, adobe등 여러 앱들이 존재한다. 그때 어떤 앱으로 열지 선택하는 창이 나오곤한다. 이미 기존에 어떤 기능을 지원하는 앱들이 있는 경우에 암시적 인텐트를 사용하여 보여주는것이 암시적 인텐트이다.

 

왜 암시적을 막아두었을까?

 💡 암시적 인텐트의 실행되는 과정, 여러 앱들의 매니페스트에 등록된 액션이 만약에 ACTION_PACKAGE_REPLACED 라고 해보자. 장치의 일부 패키지가 변경 되었다면, 모든 앱에게 패키지가 등록되었음을 알리게 된다. 이는 상당히 비효율적이다.

 

 💡 앱이 브로드캐스트를 수신하도록 등록한 경우, 앱의 수신기는 브로드캐스트가 전송될 때마다 리소스를 소비한다. 이 경우 너무 많은 앱이 시스템 이벤트 기반의 브로드캐스트를 수신하도록 등록하면 문제가 될 수 있. 브로드캐스트를 트리거하는 시스템 이벤트로 인해 모든 앱들이 급속하게 리소스를 소비할 수 있으며 이로 인해 사용자 환경이 손상될 수 있음.

 

 💡 Android 8.0(API 레벨 26) 백그라운드 실행 제한의 일환으로 API 레벨 26 이상을 타겟팅하는 앱은 암시적 브로드캐스트의 broadcast receiver를 manifest에 더 이상 등록할 수 없습니다. 하지만 현재 몇몇 브로드캐스트는 이러한 제한에서 제외됩니다. 앱이 타겟팅하는 API 레벨과 관계없이 앱은 다음 브로드캐스트의 리스너를 계속 등록할 수 있습니다. 제한 제외 액션들 https://developer.android.com/guide/components/broadcast-exceptions?hl=ko 이러한 암시적 브로드캐스트가 여전히 백그라운드에서 작동하더라도 리스너 등록을 피해야 한다고한다. ????

 

암시적 인텐트 액션들과 카테고리

 💡 카테고리는 인텐트를 처리해야 하는 구성 요소의 종류에 관한 추가 정보를 담은 문자열입니다. 인텐트 안에는 카테고리 설명이 얼마든지 들어 있을 수 있지만, 대부분의 인텐트에는 카테고리가 없어도 됩니다. 다음은 몇 가지 보편적인 카테고리입니다.

 

[CATEGORY_BROWSABLE](<https://developer.android.com/reference/android/content/Intent?hl=ko#CATEGORY_BROWSABLE>)

대상 액티비티가 웹브라우저를 통해 시작되도록 허용하고 이미지, 이메일 메시지 등의 링크로 참조된 데이터를 표시하게 합니다.

[CATEGORY_LAUNCHER](<https://developer.android.com/reference/android/content/Intent?hl=ko#CATEGORY_LAUNCHER>)

이 액티비티가 작업의 최초 액티비티이며, 시스템의 애플리케이션 시작 관리자에 목록으로 게재됩니다.

💡 위에 보여주었던 인텐트 액션중에 Broadcast Actions들이 브로드캐스트 수신자가 받을 수 있는 액션이다.

 

암시적 때문에 너무 인텐트 이야기만 해버렸다... 다시 브로드 캐스트로 돌아가자

앱에서 브로드캐스트를 전송하는 방법

sendOrderedBroadcast

sendOrderedBroadcast() 메서드는 한 번에 하나의 리시버에게 브로드캐스트를 전송하는데,

매니페스트 인텐트 필터의 android:priority를 이용해 순서대로 전송한다.

차례로 전달되기 때문에 전달 속도가 느리지만, 전파하거나 전파를 중지하는 등 전송을 제어할 수 있다. 우선순위가 동일하다면 임의의 순서로 실행된다.

문제

BroadcastReceiver 에서 메세지 클릭시 앱에서 SMS를 받고, 문자보관함으로 이동하지 않게 하는 방법은 뭐가 있을까?

  • abortBroadcast()를 사용해야한다. 이것은 sendOrderedBroadcast사용시에만 사용할 수 있다. 우선순위도 그럼 최대로 높혀놔야겠지?

sendBroadcast

sendBroadcast() 메서드는 일반 브로드캐스트 방식인데, 모든 수신자에게 브로드캐스트를 동시에 전송한다.

동시에 전송하기 때문에 전송 속도는 빠르지만 중단하거나 순서를 지정하는 등 전송을 제어할 수는 없다.

LocalBroadcastManager.sendBroadcast

LocalBroadcastManager.sendBroadcast() 메서드는 동일한 앱에 있는 수신자에 브로드캐스트를 전송한다.

앱 내부에서 전송하는 경우에는 프로세스간 통신이 필요 없고 보안 측면에서도 효율적이다.

SendBroadCast 예제

    Intent().also { intent ->
        intent.setAction("com.example.broadcast.MY_NOTIFICATION")
        intent.putExtra("data", "Notice me senpai!")
        sendBroadcast(intent)
    }

💡 putExtra로 메세지도 넣어줄 수 있다.

 

특정 앱에게만 전송하는 방법

💡 암시적 인텐트를 사용하여 민감한 정보를 브로드캐스트하지 않아야 합니다. 브로드캐스트를 수신하도록 등록한 앱에서 정보를 읽을 수 있기 때문입니다. 브로드캐스트를 수신할 수 있는 대상을 제어할 수 있는 방법은 다음과 같이 세 가지가 있습니다.

 

1. 권한으로 브로드캐스트 제한

권한을 통해 특정 권한을 보유한 앱 세트로 브로드캐스트를 제한할 수 있습니다. 브로드캐스트의 발신자 또는 수신자에 제한사항을 적용할 수 있습니다.

권한을 사용하여 전송

💡 위의 함수를 호출할 때 권한 매개변수를 지정할 수 있습니다. manifest에 태그를 사용하여 권한을 요청한 수신자(및 위험하다면 나중에 권한을 부여받은 수신자)만 브로드캐스트를 수신할 수 있습니다. 예를 들어 다음 코드는 브로드캐스트를 전송합니다.

 

sendBroadcast(Intent("com.example.NOTIFY"), 
Manifest.permission.SEND_SMS)  //얘도 스트링으로 나오던가
  • 해당 권한도 매니페스트에 선언 해놔야겠죠?

권한을 사용하여 수신

broadcast receiver를 등록할 때

로 등록해둔다면 manifest에 [](<https://developer.android.com/guide/topics/manifest/uses-permission-element?hl=ko>) 태그를 사용하여 권한을 요청한 브로드캐스터(아니면 나중에 권한을 부여받은 브로드캐스터)만 수신자에 인텐트를 전송할 수 있습니다.

예제 수신측

<receiver android:name=".MyBroadcastReceiver"
              android:permission="android.permission.SEND_SMS">
        <intent-filter>
            <action android:name="android.intent.action.AIRPLANE_MODE"/>
        </intent-filter>
    </receiver>
    var filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
    registerReceiver(receiver, filter, Manifest.permission.SEND_SMS, null )

발신측이 가지고 있어야 할 권한

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

2. setPackage(String)

적당한 예제

Intent intent = new Intent();
intent.setAction("암시적");
intent.setPackage("com.example.Receiver");
sendBroadcast(intent);

3. [LocalBroadcastManager](<https://developer.android.com/reference/androidx/localbroadcastmanager/content/LocalBroadcastManager?hl=ko>)를 사용하여 로컬 브로드캐스트를 전송할 수 있습니다.

특정 앱에게만 수신받는 방법

 💡 수신자를 등록하면 임의의 앱이 잠재적 악성 브로드캐스트를 앱의 수신자에 전송할 수 있습니다. 앱이 수신하는 브로드캐스트를 제한하는 방법은 다음과 같이 세 가지가 있습니다.

 

1. broadcast receiver를 등록할 때 권한을 지정할 수 있습니다.

2. manifest에 선언된 수신자라면 manifest에서 android:exported 속성을 'false'로 설정할 수 있습니다. 그러면 수신자는 앱 외부 소스의 브로드캐스트를 수신하지 않습니다.

3. [LocalBroadcastManager](<https://developer.android.com/reference/androidx/localbroadcastmanager/content/LocalBroadcastManager?hl=ko>)를 사용하여 수신을 로컬 브로드캐스트만으로 제한할 수 있습니다.

💡 그러나,,,, This class is deprecated. LocalBroadcastManager is an application-wide event bus and embraces layer violations in your app: any component may listen events from any other. You can replace usage of LocalBroadcastManager with other implementation of observable pattern, depending on your usecase suitable options may be LiveData or reactive streams.

그렇다고한다. 데이터 관찰/보존용으로 좀 썼나보다(?)