안드로이드의 명시적(Explicit), 암시적(Implicit) 인텐트 완벽히 이해하기!

안드로이드 인텐트

안드로이드는 인텐트(Intent)라는 객체가 있습니다. 사전적 의미를 찾아보면 '의미', '목적' 등의 의미입니다. 인텐트를 쉽게 표현하면 메시지라고 할 수 있을 것 같습니다. 많은 SDK API에서는 인텐트를 전달하고, 프레임워크 내부적으로 이 인텐트가 어떤 의도로 사용되었는지 해석을 합니다. 그리고 결과를 처리하거나 리턴해줍니다.

예를들어, 액티비티를 실행할 때 인텐트는 매우 중요합니다. 인텐트에 실행시킬 컴포넌트에 대한 정보가 모두 들어있고 ActivityManager는 이를 해석하여 어떤 액티비티를 실행하게 됩니다. 또한, 브로드캐스트를 사용할 때도 인텐트의 Action 등의 값을 전달하여 다른 앱들에게 메시지를 전달합니다. 그리고 PackageManager에 액티비티를 찾거나 앱의 정보를 얻을 때도 인텐트를 사용합니다.

명시적 인텐트(Explicit Intent)

책을 읽다 보면 암시적 인텐트(Implicit Intent)와 명시적(Explicit Intent)라는 말을 들어본 적이 있을 텐데요. 인텐트의 의미가 명확하면 명시적 인텐트라고 하고, 불명확하면 암시적 인텐트라고 합니다.

예를들어, MainActivity에서 SubActivity를 실행할 때 다음과 같은 코드를 사용합니다.

val intent = Intent(this, SubActivity::class.java)
startActivity(intent)

여기서 이 인텐트는 명시적 인텐트라고 합니다. 의도가 명확하다는 것인데요. 인텐트에 SubActivity::class.java를 인자로 넣어, 저 액티비티를 실행해달라고 명확하게 의미를 전달하였습니다. ActivityManager는 저 인텐트를 해석하여 SubActivity를 실행하게 됩니다.

암시적 인텐트(Implicit Intent)

반면에, 암시적 인텐트는 클래스명이나 패키지명을 넣어주지 않습니다. 아래 코드는 암시적 인텐트로, 디바이스에 설치된 앱들 중 액션이 ACTION_DIAL, Uri가 tel:5551212인 인텐트를 처리할 수 있는 액티비티를 찾아서 실행해줍니다.

val intent = Intent(Intent.ACTION_DIAL)
val TEST_DIAL_NUMBER = Uri.fromParts("tel", "5551212", null)
intent.setData(TEST_DIAL_NUMBER)
startActivity(intent)

디바이스에 설치된 전화앱이 실행되고, Uri에 입력된 번호 5551212가 자동으로 입력되었습니다. (전화앱에서 액티비티가 실행되면서 주어진 Uri를 읽어 번호를 입력하도록 구현이 되었을 것입니다)

image

암시적 인텐트도 ActivityManager가 인텐트의 의도에 맞는 적당한 액티비티를 찾아서 실행해줍니다. 좀 더 정확하게 말하면 ActivityManager는 PackageManager에 resolveActivity API를 호출하여 가장 적합한 액티비티가 어떤 것인지 물어봅니다. 이를 리졸빙(resolving)이라고 합니다.

리졸빙은 인텐트를 해석하여 의도에 맞는 컴포넌트를 찾는 과정을 말합니다. 다음과 같은 코드로 직접 resolveActivity를 호출하여 암시적 인텐트를 처리할 수 있는 액티비티를 알 수 있습니다.

val intent = Intent(Intent.ACTION_DIAL)
val TEST_DIAL_NUMBER = Uri.fromParts("tel", "5551212", null)
intent.setData(TEST_DIAL_NUMBER)
val info: ResolveInfo = packageManager.resolveActivity(intent, 0)
Log.d(TAG, "info : " + info)

로그는 이렇게 출력됩니다. PackageManager는 이 인텐트를 해석하여 의도에 가장 적합한 액티비티인 GoogleDialtactsActivity를 찾아냈습니다.

MainActivity: info : ResolveInfo{8b61959 com.google.android.dialer/com.google.android.apps.dialer.extensions.GoogleDialtactsActivity m=0x208000}

리졸빙 과정을 이해하려면 인텐트가 포함하는 속성과 의미를 알아야 합니다.

인텐트의 속성과 리졸빙

Action

액션(Action)은 인텐트를 대표하는 값이라고 할 수 있습니다. 인텐트는 꼭 1개의 액션만 갖을 수 있습니다. 2개 이상의 액션은 갖을 수 없습니다. 프레임워크 인텐트 코드를 보면, 아래와 같이 다양한 액션이 정의되어있고, 어떤 용도로 사용되는지 주석이 있습니다.

Intent.java
public static final String ACTION_MAIN = "android.intent.action.MAIN";
public static final String ACTION_VIEW = "android.intent.action.VIEW";
public static final String ACTION_CALL = "android.intent.action.CALL";
public static final String ACTION_SENDTO = "android.intent.action.SENDTO";
....

어떤 액션이 어떤 용도로 사용되는지는 관례적인 것 같습니다. 구글이 안드로이드 앱을 만들 때, 전화앱은 A라는 유형의 인텐트필터(바로 아래에 설명)를 사용했다면, 다른 3rd party 앱들도 구글의 샘플앱과 동일하게 인텐트필터를 설정해야 했습니다. 프레임워크 내부적으로 그런 인텐트를 사용하니, 따르지 않는다면 프레임워크와 호환이 되지 않는 앱이 될 수 있거든요.

인텐트필터(IntentFilter)는 AndroidManifest.xml에 컴포넌트(Activity 등) 태그 아래에 등록할 수 있으며, 이 컴포넌트가 어떤 인텐트를 처리할 수 있는지 PackageManager에게 알려주는 코드입니다. 만약 인텐트필터를 등록하지 않았다면, PackageManager는 암시적인텐트를 리졸빙할 때 후보군에서 그 컴포넌트를 제외하게 됩니다. 따라서 암시적 인텐트로 리졸빙이 되어야 하는 컴포넌트는 인텐트필터를 등록해야 합니다.

리졸빙을 하는 기본적인 알고리즘은 설치된 앱의 모든 인텐트필터와 인텐트를 비교하여 가장 적합한 인텐트필터를 찾습니다. 그리고 그 인텐트필터를 정의한 컴포넌트를 리턴해줍니다. 액션은 가장 첫번째로 비교하는 속성으로, 만약 인텐트의 액션과 인텐트필터의 액션이 다르면 가장 먼저 필터링되어 후보군에서 제외됩니다. 예를들어, ACTION_MAIN을 갖고 있는 인텐트를 리졸빙할 때, 이 액션을 갖고 있는 인텐트필터를 대상으로 다음 속성을 체크합니다.

Category

카테고리는 리졸빙과정에서 사용되는 두번째 속성입니다. 액션과 다르게 인텐트필터는 카테고리를 1개 이상 설정할 수 있습니다. 암시적 인텐트의 리졸빙 대상이 되려면 인텐트필터에 꼭 CATEGORY_DEFAULT가 포함되어야 합니다. 인텐트필터가 인텐트의 리졸빙 대상이 되려면, 인텐트필터는 인텐트가 갖고 있는 카테고리를 모두 갖고 있으면 됩니다.

예를들어 보면, 다음과 같은 인텐트필터가 선언되어 있습니다. 액션은 제가 임의로 codechacha.intent.action.TEST로 정하였습니다. 그리고 카테고리는 단계적으로 1개씩 새로운 카테고리가 추가되었습니다.

<activity android:name=".SubActivity">
    <intent-filter>
        <action android:name="codechacha.intent.action.TEST"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>
<activity android:name=".SubActivity2">
    <intent-filter>
        <action android:name="codechacha.intent.action.TEST"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.APP_BROWSER"/>
    </intent-filter>
</activity>
<activity android:name=".SubActivity3">
    <intent-filter>
        <action android:name="codechacha.intent.action.TEST"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.APP_BROWSER"/>
        <category android:name="android.intent.category.APP_CALCULATOR"/>
    </intent-filter>
</activity>

다음과 같이 새로운 카테고리가 1개씩 추가된 인텐트들로 리졸빙을 해보았습니다. 첫번째 인텐트는 DEFAULT 카테고리만 갖고 있습니다. 그렇기 때문에 3개의 인텐트필터가 모두 리졸빙의 대상이 됩니다. 두번째 인텐트는 DEFAULTAPP_BROWSER를 갖고 있습니다. 첫번째 인텐트필터는 대상에서 제외되지만 두번째와 세번째는 대상에 포함됩니다. 세번째 인텐트는 카테고리가 3개입니다. 마지막 인텐트필터만 리졸빙 대상에 포함됩니다.

(참고로, Intent객체에 카테고리를 아무것도 설정해주지 않으면 프레임워크 내부적으로 CATEGORY_DEFAULT를 설정해줍니다.)

val intent = Intent("codechacha.intent.action.TEST")
intent.addCategory(Intent.CATEGORY_DEFAULT)
val info: ResolveInfo = packageManager.resolveActivity(intent, 0)
Log.d(TAG, "1) : " + info)

val intent2 = Intent("codechacha.intent.action.TEST")
intent2.addCategory(Intent.CATEGORY_DEFAULT)
intent2.addCategory(Intent.CATEGORY_APP_BROWSER)
val info2: ResolveInfo = packageManager.resolveActivity(intent2, 0)
Log.d(TAG, "2) : " + info2)

val intent3 = Intent("codechacha.intent.action.TEST")
intent3.addCategory(Intent.CATEGORY_DEFAULT)
intent3.addCategory(Intent.CATEGORY_APP_BROWSER)
intent3.addCategory(Intent.CATEGORY_APP_CALCULATOR)
val info3: ResolveInfo = packageManager.resolveActivity(intent3, 0)
Log.d(TAG, "3) : " + info3)

resolveActivity의 결과로 무엇이 출력되었는지 로그를 볼까요. 첫번째와 두번째 인텐트는 결과로 ResolverActivity가 리턴되었습니다. 세번째는 SubActivity3가 리턴되었습니다. 위에서 리졸빙 대상에 대해서 얘기해보았는데요, 첫번째와 두번째 인텐트는 리졸빙 대상이 두개 이상입니다. PackageManager는 이 두개 이상의 인텐트필터로 대상을 좁혔지만 이 중에 어떤 액티비티가 가장 적합한지 결정을 못했습니다. 그래서 사용자보고 정해달라는 의미로 ResolverActivity를 리턴하였습니다. 세번째는 리졸빙 대상이 1개뿐이 없기 때문에 SubActivity3가 결과로 리턴되었습니다.

MainActivity: 1) : ResolveInfo{8b61959 android/com.android.internal.app.ResolverActivity m=0x0}
MainActivity: 2) : ResolveInfo{518191e android/com.android.internal.app.ResolverActivity m=0x0}
MainActivity: 3) : ResolveInfo{e1e05ff com.codechacha.intent/.SubActivity3 m=0x108000}

ResolverActivity가 무엇인지 궁금하실 수 있는데요. 저희가 자주보는 ChooserActivity와 같은 것입니다. 아래 코드로 실행하면, 리졸빙이 되지 않아 ResolverActivity가 실행됩니다. 3개중 1개를 선택해야 하는데, 이 인텐트의 리졸빙 후보군이 3개이기 때문입니다.

val intent = Intent("codechacha.intent.action.TEST")
intent.addCategory(Intent.CATEGORY_DEFAULT)
startActivity(intent)

image

Data

데이터는 URI를 말하며, URI는 아래와 같이 4개의 파트로 구성됩니다.

<scheme>://<host>:<port>/<path>

ex)
content://com.example.project:200/folder/subfolder/etc

데이터를 갖고 있는 인텐트를 생성하는 코드는 아래와 같습니다. Uri를 스트링으로 알고 있으면 Uri.parse를 통해 Uri객체로 만들 수 있습니다. 그리고 setData로 Uri를 설정할 수 있습니다.

val intent = Intent()
val uri = Uri.parse("content://com.example.project:200/folder/subfolder/etc")
intent.setData(uri)
Log.d(TAG, intent.data.toString())

Intent.data를 로그로 출력해보면 아래처럼 출력됩니다.

MainActivity: content://com.example.project:200/folder/subfolder/etc

MIME Type

MIME 타입은 video/mpeg 또는 image/* 등과 같은 데이터의 타입을 표현합니다. 타입은 개발자가 마음대로 정의할 수 있지만, 다른 앱에서 사용할 수 있는 앱을 만든다면, 안드로이드에서 관례적으로 사용하는 것을 사용해야 합니다.

AndroidManifest.xml에서 인텐트필터는 아래처럼 정의할 수 있습니다.

<intent-filter>
    <data android:mimeType="video/mpeg" android:scheme="http" ... />
    <data android:mimeType="audio/mpeg" android:scheme="http" ... />
    ...
</intent-filter>

코드로 MIME 타입을 설정하려면 아래처럼 하시면 됩니다.

val intent = Intent()
intent.setType("video/mp4")

MIME타입과 Uri를 함께 설정하려면 setDataAndType를 사용해야 합니다. 각각 따로 설정하면 마지막에 설정한 속성만 적용되고 다른 속성은 삭제됩니다.

intent.setDataAndType(uri, "video/mp4")

리졸빙

바로 위에서 카테고리 속성을 다룰 때 리졸빙에 대해서 간략히 설명하였습니다. 액션과 카테고리만 있으면 리졸빙을 이해하기는 쉽지만, DataMIME Type이 추가되면 조금 헷갈립니다. 또, 시간이 지나면 까먹어서 기억이 안나니 더 헷갈리거든요. DataMIME Type은 나중에 다루기로 하고, 이번 글에서는 Action과 Category에 대해서만 리졸빙을 설명하겠습니다.

리졸빙(Resolving)이란 단어는 사용되지 않지만 프레임워크 내부적으로 인텐트를 해석할 때 resolve라는 단어를 사용합니다. 그래서 리졸빙을 정의하자면, 인텐트에 맞는 컴포넌트를 찾는 과정이라고 할 수 있습니다.

리졸빙을 이해하기 앞서, PackageManager의 쿼리(Query)에 대해서 알아야 합니다. PackageManager는 queryIntentActivities라는 API를 제공합니다. 인텐트와 연관이 있는 액티비티들을 찾아주는 API입니다. 카테고리 속성을 설명할 때 사용한 앱에서 다음과 같은 코드로 쿼리를 해보겠습니다.

val intent = Intent("codechacha.intent.action.TEST")
intent.addCategory(Intent.CATEGORY_DEFAULT)
var resolveInfo = packageManager.queryIntentActivities(intent, 0)
resolveInfo?.forEach({it ->
    Log.d(TAG, "info: ${it.toString()}")
})

인텐트에 대해서 쿼리를 하고 그 결과를 출력하는 코드입니다. 아래의 출력 로그를 보면 3개의 모든 SubActivity가 쿼리 결과에 포함되었습니다. 인자로 넘겨진 인텐트의 카테고리가 디폴트만 갖고 있기 때문에 3개의 인텐트필터를 통과하였고 결과로 출력되었습니다.

2018-12-06 20:11:06.990 4638-4638/com.codechacha.intent D/MainActivity: info: ResolveInfo{25b9e6b com.codechacha.intent/.SubActivity m=0x108000}
2018-12-06 20:11:06.990 4638-4638/com.codechacha.intent D/MainActivity: info: ResolveInfo{d093dc8 com.codechacha.intent/.SubActivity2 m=0x108000}
2018-12-06 20:11:06.991 4638-4638/com.codechacha.intent D/MainActivity: info: ResolveInfo{e726e61 com.codechacha.intent/.SubActivity3 m=0x108000}

쿼리를 하게 되면 내부적으로 디바이스에 설치된 모든 앱의 액티비티를 비교하게 됩니다. 로그상에 m=으로 보이는 16진수는 인텐트와 인텐트필터가 얼마나 유사한지 숫자로 표현한 것입니다. 0이상이면 일치하는 것이고 0이하이면 전혀 일치하지 않는다는 의미입니다.

쿼리는 매치에 대한 점수를 매기고, 전혀 일치하지 않는 것을(match가 0이하인 값) 필터링합니다. 그리고 리졸빙은 걸러진 컴포넌트 중 누구를 실행할지 결정하는 과정입니다. 후보군들 중 특별한 컴포넌트가 없으면, ResolverActivity를 띄워 사용자가 선택하게 합니다. 하지만 이전에 사용자가 선택하였었다면 시스템은 다시 물어보지 않고 이전에 선택한 컴포넌트를 선택합니다.

코드로 확인해볼께요. 아래코드를 실행시키면 ResolverActivity가 뜨게 됩니다. 첫번째 액티비티를 선택하고 항상 이 액티비티로 띄우겠다는 의미인 always 버튼을 누릅니다.

val intent = Intent("codechacha.intent.action.TEST")
intent.addCategory(Intent.CATEGORY_DEFAULT)
startActivity(intent)

지금 사용자가 선호하는(Preferred) 액티비티를 선택했기 때문에 다시 저 코드를 실행하면 ResolverActivity가 뜨지 않고 선택한 첫번째 액티비티가 실행됩니다. 다시 실행해보세요. ResolverActivity가 뜨지 않고 바로 첫번째 액티비티가 뜹니다.

코드로 확인해볼까요. 이번에는 resolveActivity로 어떤 것이 출력되는지 확인해보겠습니다. resolveActivity는 쿼리한 결과 중 가장 적합한 액티비티를 찾아주는 API라고 했습니다.

val intent = Intent("codechacha.intent.action.TEST")
intent.addCategory(Intent.CATEGORY_DEFAULT)
val info3: ResolveInfo = packageManager.resolveActivity(intent, 0)
Log.d(TAG, "info " + info3)

로그를 보면 ResolveActivity가 리턴되지 않고 SubActivity가 리턴되었습니다. 쿼리를 통해서 후보군이 3개로 좁혀졌고, 사용자가 3개의 액티비티 중에 첫번째 것을 선택하였기 때문에 물어보지 않고 첫번째 액티비티가 리졸빙의 결과로 출력되었습니다.

MainActivity: info ResolveInfo{25b9e6b com.codechacha.intent/.SubActivity m=0x108000}

정리

인텐트, 쿼리, 리졸빙에 대해서 간략히 알아보았습니다. 인텐트의 액션과 카테고리만 사용한다면 인텐트필터를 설계하는 것은 어렵지 않습니다. 하지만 DATA와 MIME을 추가한다면 인텐트필터 설계가 조금 까다로울 수 있습니다. 암시적 인텐트라는 말처럼 리졸빙은 항상 정확하지 않습니다. 의도와 다르게 실행된다면 인텐트의 정의가 잘못되었거나 더 유사한 액티비티가 디바이스에 존재하기 때문일 수 있습니다. 이런 문제를 디버깅하는 좋은 방법은 PackageManager의 queryIntentActivitiesresolveActivity를 적절히 사용하는 것입니다.

Data와 MIME 타입에 대해서는 다음 글 안드로이드의 명시적(Explicit), 암시적(Implicit) 인텐트 완벽히 이해하기! (2)에서 이어나가겠습니다.

참고

Loading script...

Related Posts

codechachaCopyright ©2019 codechacha