
해당 포스팅은 Android 공식문서를 읽고 정리한 내용을 바탕으로 작성하였습니다.
https://developer.android.com/reference/android/app/PendingIntent
PendingIntent | Android Developers
developer.android.com
A description of an Intent and target action to perform with it. Instances of this class are created with getActivity(Context, int, Intent, int), getActivities(Context, int, Intent, int), getBroadcast(Context, int, Intent, int), and getService(Context, int, Intent, int); the returned object can be handed to other applications so that they can perform the action you described on your behalf at a later time.
PendingIntent는 getActivity(), getActivities(), getBroadcast(), getService() 메서드를 통해 생성할 수 있다.
PendingIntent에 정의된 action(액티비티를 실행시켜라!, 서비스를 실행시켜라! 등)은 다른 애플리케이션에서 수행될 수 있다.
By giving a PendingIntent to another application, you are granting it the right to perform the operation you have specified as if the other application was yourself (with the same permissions and identity). As such, you should be careful about how you build the PendingIntent: almost always, for example, the base Intent you supply should have the component name explicitly set to one of your own components, to ensure it is ultimately sent there and nowhere else.
다른 애플리케이션에 PendingIntent를 전달하면, 마치 자신의 Intent인 것처럼 작업을 수행할 권한을 얻는다.
예를 들어, PendingIntent를 통해 다른 앱의 BroadcastReceiver로 intent를 전달할 수 있다.
반드시 자신의 애플리케이션이 아니어도 시스템이 유지하고 있는 PendingIntent를 다른 애플리케이션으로 전달 가능하다.
다른 앱에서 작업을 실행할 수 있는 만큼, 보안에 각별히 귀 기울여야 한다.
A PendingIntent itself is simply a reference to a token maintained by the system describing the original data used to retrieve it. This means that, even if its owning application's process is killed, the PendingIntent itself will remain usable from other processes that have been given it. If the creating application later re-retrieves the same kind of PendingIntent (same operation, same Intent action, data, categories, and components, and same flags), it will receive a PendingIntent representing the same token if that is still valid, and can thus call
cancel()
to remove it.
PendingIntent는 시스템에서 유지하는 토큰에 대한 참조일 뿐이다. 자체적으로 무언가 할 수 있는 객체는 아니다.
중요한 것은 PendingIntent가 감싸고 있는 Intent에 정의된 여러 데이터 집합들이라는 것이다.
이 말은 즉슨, PendingIntent를 생성한 애플리케이션이 종료되어도, 시스템에서 이를 유지하기 때문에 다른 애플리케이션에서 해당 PendingIntent를 제공받아 작업을 할 수 있다.
동일한 operation, action, data, categories, components, flag으로 정의된 intent를 보유한 PendingIntent는 같다.
따라서 해당 PendingIntent를 cancel() 메서드를 통해 제거할 수 있다.
Because of this behavior, it is important to know when two Intents are considered to be the same for purposes of retrieving a PendingIntent. A common mistake people make is to create multiple PendingIntent objects with Intents that only vary in their "extra" contents, expecting to get a different PendingIntent each time. This does not happen. The parts of the Intent that are used for matching are the same ones defined by Intent.filterEquals. If you use two Intent objects that are equivalent as per Intent.filterEquals, then you will get the same PendingIntent for both of them.
val one = Intent(this, MainActivity::class.java)
.putExtra("key", "Hello, One!")
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val onePendingIntent = PendingIntent.getActivity(
this, 51, one,
PendingIntent.FLAG_IMMUTABLE
)
val other = Intent(this, MainActivity::class.java)
.putExtra("key", "Hello, Other!")
.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val otherPendingIntent = PendingIntent.getActivity(
this, 51, other,
PendingIntent.FLAG_IMMUTABLE
)
onePendingIntent.cancel()
otherPendingIntent.send() // RuntimeException: Unable to start activity!
위 코드에서 one과 other Intent는 서로 다른 객체이다.
따라서 이를 PendingIntent에 전달하면, 다른 PendingIntent를 반환받을 것 같지만 그렇지 않다.
동일한 Request Code, intent.filterEquals()가 참인 Intent에 대해서는 동일한 PendingIntent를 반환받는다.
가령, 위 코드처럼 putExtra()를 통해 다른 extra 데이터를 전달한다고 하더라도 intent.filterEquals()는 참을 반환한다.
뿐만 아니라, 동일한 Request Code로 PendingIntent를 생성하고 있으므로 onePendingIntent와 otherPendingIntent는 같다.
그런 이유로 otherPendingIntent.send()를 호출하면 취소된 PendingIntent를 실행시키려 하니 앱이 터지게 되는 것이다.
확실하게 이 둘이 같은 객체인지 알아보고 싶다면 아래 코드를 실행해 보라.
Log.d("buna", onePendingIntent.hashCode().toString())
Log.d("buna", otherPendingIntent.hashCode().toString())

공교롭게도 둘은 같은 객체이다.
추가로 알면 좋은 내용으로, Request Code는 PendingIntent를 구분하기 위한 고유 식별자 역할을 한다.
그렇기에 이 둘이 다르면, 다른 PendingIntent를 반환한다.

또한, filterEquals() 메서드는 조금 생소할 수 있다.
이 메서드는 두 Intent가 같은 메서드인지 반환한다.
Log.d("buna", one.filterEquals(other).toString())
Log.d("buna", one.hashCode().toString())
Log.d("buna", other.hashCode().toString())

결과를 보면 알 수 있다시피, 둘은 같은 Intent로 취급된다.
하지만 hashCode() 값이 알려주듯, 실제로는 같은 객체가 아니라는 사실에 주의해야 한다.
그저 동일한 PendingIntent를 반환할 것인지에 대한 척도로써 같은 Intent로 취급한다는 것이다.

Intent.filterEquals() 메서드에 들어가 보면 비교 대상 코드가 가독성 높게 잘 작성되어 있다.
주의 깊게 살펴보아야 할 것은 Intent를 사용하면서 가장 자주 접하는 Extra data, Flags은 비교 대상이 아니라는 점이다.
따라서 Notification 등을 보낼 때 Extra data만 다르게 하고 다른 PendingIntent를 반환받기 기대하면 안 된다.
There are two typical ways to deal with this.
If you truly need multiple distinct PendingIntent objects active at the same time (such as to use as two notifications that are both shown at the same time), then you will need to ensure there is something that is different about them to associate them with different PendingIntents. This may be any of the Intent attributes considered by Intent.filterEquals, or different request code integers supplied to getActivity(Context, int, Intent, int), getActivities(Context, int, Intent, int), getBroadcast(Context, int, Intent, int), or getService(Context, int, Intent, int).
위와 동일한 내용이다.
정리하자면, PendingIntent를 비교하는 방식은 크게 2가지다.
- Intent.filterEquals()
- Request code
getActivity(), getActivities(), getBroadcast(), getService()에 따라서도 다른 PendingIntent를 반환한다.
또한, PendingIntent를 만들 때 전달하는 Flag에 따라서도 다른 PendingIntent를 반환하는 경우도 있다.
e.g. PendingIntent.FLAG_IMMTUABLE과 PendingIntent.FLAG_MUTABLE 은 위 2가지 조건이 같은 경우에도, 다른 PendingIntent를 반환한다.
If you only need one PendingIntent active at a time for any of the Intents you will use, then you can alternatively use the flags FLAG_CANCEL_CURRENT or FLAG_UPDATE_CURRENT to either cancel or modify whatever current PendingIntent is associated with the Intent you are supplying.
Also note that flags like FLAG_ONE_SHOT or FLAG_IMMUTABLE describe the PendingIntent instance and thus, are used to identify it. Any calls to retrieve or modify a PendingIntent created with these flags will also require these flags to be supplied in conjunction with others. E.g. To retrieve an existing PendingIntent created with FLAG_ONE_SHOT, both FLAG_ONE_SHOT and FLAG_NO_CREATE need to be supplied.
PendingIntent를 생성할 때 Flag를 적절히 적용하는 것이 중요하다.
정말 다양한 Flag가 존재하는데, 예를 들어, FLAG_CANCEL_CURRENT 또는 FLAG_UPDATE_CURRENT는 각각 기존 PendingIntent를 취소하고 새로 생성하거나, Extra 데이터를 업데이트하는 역할을 수행한다.
따라서 한 번에 하나의 PendingIntent만 active 하게 만들 수 있다.
- PendingIntent.CanceledException
- 취소된 PendingIntent를 실행하고자 할 때 발생하는 Exception이다.
- PendingIntent.OnFinished
- 전송이 올바르게 되었을 때 호출되는 Callback 인터페이스다.
- fun onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode, String resultData, Bundle resultExtras)
- pendingIntent : 전달된 PendingIntent
- intent : PendingIntent가 감싸고 있던 Intent
- resultCode : 전송에 의해 결정된 최종 결과 코드
- resultData : Broadcast에 의해 수집된 마지막 ResultData
- resultExtras : Broadcast에 의해 수집된 마지막 Extra 데이터
- PendingIntent.send() : PendingIntent의 Intent 작업을 실행할 수 있다.
- PendingIntent.cancel() : 현재 활성화된 PendingIntent를 취소할 수 있다. PendingIntent를 소유한 원래 애플리케이션에서만 취소할 수 있다. 만약 취소 후에 send() 한다면 PendingIntent.CanceledException 발생한다.
- PendingIntent.getIntentSender() : PendingIntent 발신자에 대한 정보를 가진다. PendingIntent를 생성한 애플리케이션의 package, UID 등 정보를 알 수 있다.
Flags
PendingIntent를 생성할 때, 함께 전달하는 인자이다.
어떤 Flag를 지정하느냐에 따라 PendingIntent의 성격이 달라진다.
- PendingIntent.FLAG_UPDATE_CURRENT
- 시스템이 유지하고 있는 PendingIntent가 있다면, 해당 PendingIntent의 Extra data를 업데이트한다.
- PendingIntnet.FLAG_CANCEL_CURRENT
- 시스템이 유지하고 있는 PendingIntent가 있다면 취소하고 다시 생성한다.
- 기존 Intent의 Extra data를 업데이트하고 싶을 때 사용할 수 있다. (like PendingIntent.FLAG_UPDATE_CURRENT)
- PendingIntent.FLAG_NO_CREATE
- 시스템이 유지하고 있는 PendingIntent가 없다면, 굳이 새롭게 생성하지 않는다.
- 수신하는 곳에서 Intent가 null이다.
- PendingIntent.FLAG_ONE_SHOT
- PendingIntent를 오직 한 번만 사용한다.
- 만약 동일한 PendingIntent에 대해 재호출(e.g. PendingIntent.send() 두 번 호출)하면 예외가 발생한다.
- PendingIntent.FLAG_IMMUTABLE
- PendingIntent.send()에 intent를 전달해도 값을 채울 수 없다.
- 공식문서에서는 해당 FLAG 사용을 강력히 권장한다. 그 이유는 다른 어플리케이션에서 PendingIntent의 Intent를 함부로 수정하지 못하게 함으로써 보안을 높일 수 있기 때문이다. (신용 카드를 남에게 주고 대신 결제해주길 기대하는 상황과 같음.)
- PendingIntent 생성자(Creator)는 항상 PendingIntent.FLAG_UPDATE_CURRENT를 함께 사용하여 자체 PendingIntent를 변경할 수 있다.
- PendingIntent.FLAG_MUTABLE
- PendingIntent.send()에 intent를 전달하여 채워지지 않은 Extra Data를 추가할 수 있다.
- API 31 이전에는 따로 설정하지 않으면 기본값이 MUTABLE이다.
- Notification의 RemoteInput(알림에서 답장), Bubble(도움말 풍선) 처럼 사용자 입력 텍스트를 intent에 추가(= 수정)해야 하는 경우에만 사용해야 한다.
- For security reasons, the Intent objects you supply here should almost always be explicit intents, that is specify an explicit component to be delivered to through Intent.setClass
- 보안상의 이유로, Intent.setClass() 를 통해 항상 명시적 인텐트를 지정해주어야 한다. (자세한 이유는 보안 문제를 겪지 않아봐서 모르겠다. 추측으로는 send() 메서드는 채워지지 않은 값들을 채울 수 있기 때문에, 다른 Package와 Component를 악의적으로 채워넣어 악성 애플리케이션을 실행하는 시나리오로 이어질 수 있기 때문이라고 생각한다.)
+) FLAG_IMMUTABLE과 FLAG_MUTABLE은 함께 사용할 수 없다.
+) API 31+부터 둘 중 하나를 지정해주지 않으면 런타임 에러가 발생한다.
+) PendingIntent는 객체 생성과 동시에 시스템에 유지되기 시작한다.
PendingIntent.send()는 일반적으로 직접 호출하지는 않는다.
해당 메서드는 PendingIntent가 감싸고 있는 Intent의 작업을 실행시키는 것인데, 바로 send() 해서 작업을 실행할 것이라면 굳이 PendingIntent로 만들 이유가 없기 때문이다.
그럼 해당 메서드는 누가 call() 하는가?
Notification을 대표적인 예로 들 수 있다.
NotificationCompat.notify()를 호출하면 시스템은 알림을 표시하고, 사용자가 알림을 누르면 비로소 PendingIntent.send()를 호출하여 작업을 실행한다.
이 과정에서, 다른 Intent 값으로 변질되지 않도록 FLAG_IMMUTABLE을 사용한다.
PendingIntent 취약점에 관한 게시글
인텐트 리디렉션 취약점 해결
PendingIntent 취약점에 관하여
'Android' 카테고리의 다른 글
[Android] BroadcastReceiver 보안 이슈 (1) | 2023.05.17 |
---|---|
[Android] API 33 onBackPressed() deprecated (0) | 2023.05.08 |
[Android] RecyclerView Animation (LayoutAnimation, ItemAnimator) (0) | 2023.04.28 |
[Android] Listview vs RecyclerView (1) | 2023.04.20 |
[Android] This version of the Android Support plugin for IntelliJ IDEA (or Android Studio) 오류 해결 (2) | 2022.12.23 |