HOME > android > basic

안드로이드의 명시적, 암시적 인텐트(Explicit, Implicit Intent) (2)

JSFollow06 Dec 2018

이전 글 안드로이드 명시적, 암시적 인텐트(Explicit, Implicit Intent)에서는 인텐트의 구성요소와, ACTION과 CATEGORY의 리졸빙에 대해서 알아보았습니다. 이번에는 DATA와 MIME타입의 리졸빙에 대해서 알아보겠습니다.

AndroidManifest.xml의 인텐트필터는 다음과 같이 설정하였습니다. 저와 동일한 코드로 확인을 해보시려면 필터도 동일하게 설정해주세요.

<activity android:name=".SubActivity">
     <intent-filter>
         <action android:name="android.intent.action.VIEW"/>
         <category android:name="android.intent.category.DEFAULT"/>
         <category android:name="android.intent.category.BROWSABLE" />
         <data android:scheme="http" />
     </intent-filter>
 </activity>
 <activity android:name=".SubActivity2">
     <intent-filter>
         <action android:name="android.intent.action.VIEW"/>
         <category android:name="android.intent.category.DEFAULT"/>
         <category android:name="android.intent.category.BROWSABLE" />
         <data android:scheme="http" />
         <data android:scheme="https" />
         <data android:host="foo.com" />
     </intent-filter>
 </activity>
 <activity android:name=".SubActivity3">
     <intent-filter>
         <action android:name="android.intent.action.VIEW"/>
         <category android:name="android.intent.category.DEFAULT"/>
         <category android:name="android.intent.category.BROWSABLE" />
         <data android:scheme="http" />
         <data android:scheme="https" />
         <data android:host="foo.com" />
         <data android:path="/apps" />
     </intent-filter>
 </activity>

Data

데이터(Data)의 구조는 다음과 같습니다. scheme, host, port, path로 구분할 수 있습니다.

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

ex)
https://foo.com:200/folder/subfolder/etc

먼저 URI http://를 데이터로 설정하고 쿼리를 해보세요. 이 데이터에는 scheme뿐이 없습니다.

val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
val uri = Uri.parse("http://")
intent.setData(uri)

Log.d(TAG, "uri: ${uri.toString()}")
var results = packageManager.queryIntentActivities(intent, 0)
results?.forEach { resolveInfo ->
    Log.d(TAG, "info: $resolveInfo")
}

로그를 보면 결과에 내 앱과 다른 앱의 액티비티가 모두 포함되어 리턴되었습니다.

MainActivity: uri: http://
MainActivity: info: ResolveInfo{d3c504 com.google.android.setupwizard/.util.WebDialogActivity p=5 m=0x208000}
MainActivity: info: ResolveInfo{54a4aed com.android.chrome/com.google.android.apps.chrome.IntentDispatcher m=0x208000}
MainActivity: info: ResolveInfo{3c44d22 org.chromium.webview_shell/.WebViewBrowserActivity m=0x208000}
MainActivity: info: ResolveInfo{46163b3 com.codechacha.intent/.SubActivity m=0x208000}
MainActivity: info ResolveInfo{94a2d70 com.google.android.setupwizard/.util.WebDialogActivity p=5 m=0x208000}

지금은 내 앱의 인텐트필터만 확인하고 싶습니다. setPackage에 내 앱의 패키지네임을 넣어주면 내 앱의 액티비티에 대해서만 암시적 인텐트로 쿼리를 합니다. 아래 코드로 다시 쿼리 결과를 확인해보세요.

val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
val uri = Uri.parse("http://")
intent.setData(uri)
intent.setPackage(packageName)

Log.d(TAG, "uri: ${uri.toString()}")
var results = packageManager.queryIntentActivities(intent, 0)
results?.forEach { resolveInfo ->
    Log.d(TAG, "info: $resolveInfo")
}

로그를 보면 쿼리 결과에 내 앱의 액티비티만 있는 것을 확인할 수 있습니다.

MainActivity: uri: http://
MainActivity: info: ResolveInfo{d3c504 com.codechacha.intent/.SubActivity m=0x208000}

결과를 보면 3개의 액티비티 중 첫번째 SubActivity만 포함되었습니다. 쿼리 결과에 포함이 되면 인텐트필터를 통과하여 액티비티가 처리 가능하다는 의미입니다. 포함되지 않으면, 인텐트필터가 처리할 수 없는 인텐트입니다.

앞으로 결과에 액티비티가 포함된 것을 인텐트와 일치했다라고 표현하고, 포함되지 않은 것을 일치하지 않았다라고 표현하겠습니다.

그럼 위의 인텐트는 왜 첫번째 액티비티만 일치했을까요? 필터를 비교해보면 SubActivity는 scheme이 http만 있고 SubActivity2는 http와 https 둘다 있습니다. 인텐트필터에 정의된 scheme을 볼 때, 데이터가 https://라면 SubActivity는 불일치, SubActivity2는 일치한다는 것을 예상할 수 있습니다. 하지만 왜 데이터가 http://인데도 SubActivity2는 불일치했을까요?

<activity android:name=".SubActivity">
    <intent-filter>
        ...
        <data android:scheme="http" />
    </intent-filter>
</activity>
<activity android:name=".SubActivity2">
    <intent-filter>
        ...
        <data android:scheme="http" />
        <data android:scheme="https" />
        <data android:host="foo.com" />
    </intent-filter>
</activity>

답은 SubActivity2에 host가 정의되어 있기 때문입니다. 필터에 host를 설정하지 않으면 모든 host를 처리할 수 있다는 의미입니다. 반면에 host를 설정하면 그 값만 처리할 수 있다는 의미입니다. SubActivity는 host가 없기 때문에 모든 호스트를 처리할 수 있고, SubActivity2는 foo.com만 처리할 수 있습니다. 따라서 데이터 http://의 host는 foo.com이 아니기 때문에 SubActivity2는 불일치하였습니다.

다음은 아래 코드로 쿼리를 해보세요. 데이터가 https://foo.com로 변경되었고 나머지는 동일합니다.

val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
val uri = Uri.parse("http://foo.com")
intent.setData(uri)
intent.setPackage(packageName)

Log.d(TAG, "uri: ${uri.toString()}")
var results = packageManager.queryIntentActivities(intent, 0)
results?.forEach { resolveInfo ->
    Log.d(TAG, "info: $resolveInfo")
}

SubActivity2만 일치하고 나머지는 일치하지 않네요.

MainActivity: uri: https://foo.com
MainActivity: info: ResolveInfo{d3c504 com.codechacha.intent/.SubActivity2 m=0x308000}

방금 위에서 말씀드린 것처럼 데이터의 scheme을 https로 변경하였고, SubActivity의 인텐트필터는 https를 host로 설정하지 않아서 불일치하였습니다.

scheme만 볼 때, SubActivity3도 일치해야 할 것 같은데, 제외되었습니다. 그 이유는 path입니다. path도 host와 동일하게 인텐트필터에서 설정하지 않으면 모든 패스를 처리할 수 있다는 의미이고, 설정하면 그 패스만 처리할 수 있다는 의미입니다. 따라서, SubActivity3이 불일치한 이유는 path를 /apps로 설정하였기 때문입니다.

<activity android:name=".SubActivity2">
    <intent-filter>
        ...
        <data android:scheme="http" />
        <data android:scheme="https" />
        <data android:host="foo.com" />
    </intent-filter>
</activity>
<activity android:name=".SubActivity3">
    <intent-filter>
        ...
        <data android:scheme="http" />
        <data android:scheme="https" />
        <data android:host="foo.com" />
        <data android:path="/apps" />
    </intent-filter>
</activity>

그럼 3개의 필터가 모두 일치하도록 쿼리를 해보겠습니다. 공통적으로 겹치는 URI는 http://foo.com입니다. 아래 코드로 쿼리를 해보세요.

val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
val uri = Uri.parse("http://foo.com/apps")
intent.setData(uri)
intent.setPackage(packageName)

Log.d(TAG, "uri: ${uri.toString()}")
var results = packageManager.queryIntentActivities(intent, 0)
results?.forEach { resolveInfo ->
    Log.d(TAG, "info: $resolveInfo")
}

로그를 보면 모든 필터와 일치하여 결과에 포함되었습니다.

MainActivity: uri: http://foo.com/apps
MainActivity: info: ResolveInfo{d3c504 com.codechacha.intent/.SubActivity3 m=0x508000}
MainActivity: info: ResolveInfo{54a4aed com.codechacha.intent/.SubActivity2 m=0x308000}
MainActivity: info: ResolveInfo{3c44d22 com.codechacha.intent/.SubActivity m=0x208000}

MIME Type

MIME Type은 데이터의 타입을 의미합니다. MIME타입은 좀 더 섬세하게 리졸빙하는데 사용되는 속성입니다. 인텐트필터를 아래처럼 설정하였습니다.

<activity android:name=".SubActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="http" />
        <data android:mimeType="audio/mp3"/>
    </intent-filter>
</activity>
<activity android:name=".SubActivity2">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="http" />
        <data android:mimeType="audio/ogg"/>
    </intent-filter>
</activity>
<activity android:name=".SubActivity3">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="http" />
        <data android:mimeType="audio/*"/>
    </intent-filter>
</activity>

그리고 아래 코드처럼 쿼리를 해보았습니다. 데이터와 MIME타입을 동시에 설정하였습니다.

val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
val uri = Uri.parse("http://foo.com/apps")
val mimetype = "audio/mp3"
intent.setDataAndType(uri, mimetype)
intent.setPackage(packageName)

Log.d(TAG, "uri: ${uri.toString()}")
var results = packageManager.queryIntentActivities(intent, 0)
results?.forEach { resolveInfo ->
    Log.d(TAG, "info: $resolveInfo")
}

MIME타입이 audio/mp3라서, SubActivity2는 불일치하였네요. 먼저 위에서 배운 데이터(DATA)로 일치여부를 체크하고 MIME타입이 일치하는지 체크한 후 결과로 리턴합니다. SubActivity3은 MIME타입이 audio/*인데, *을 붙이면 어떤 타입이든 처리할 수 있다는 의미입니다.

MainActivity: uri: http://foo.com/apps
MainActivity: info: ResolveInfo{829b71f com.codechacha.intent/.SubActivity m=0x608000}
MainActivity: info: ResolveInfo{889a6c com.codechacha.intent/.SubActivity3 m=0x608000}

*을 검증하기 위해 아래 코드처럼 인텐트 필터에 정의되어있지 않은 audio/abc로 쿼리를 해보았습니다.

val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
val uri = Uri.parse("http://foo.com/apps")
val mimetype = "audio/abc"
intent.setDataAndType(uri, mimetype)
intent.setPackage(packageName)

Log.d(TAG, "uri: ${uri.toString()}")
var results = packageManager.queryIntentActivities(intent, 0)
results?.forEach { resolveInfo ->
    Log.d(TAG, "info: $resolveInfo")
}

예상한 것처럼 SubActivity3만 리턴되었네요.

MainActivity: uri: http://foo.com/apps
MainActivity: info: ResolveInfo{8e50b55 com.codechacha.intent/.SubActivity3 m=0x608000}

지금은 인텐트필터의 *에 대해서 확인해보았습니다. 하지만 아래처럼 쿼리하려는 인텐트에 MIME타입으로 audio/*을 넣으면 어떻게 될까요?

val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
val uri = Uri.parse("http://foo.com/apps")
val mimetype = "audio/*"
intent.setDataAndType(uri, mimetype)
intent.setPackage(packageName)

Log.d(TAG, "uri: ${uri.toString()}")
var results = packageManager.queryIntentActivities(intent, 0)
results?.forEach { resolveInfo ->
    Log.d(TAG, "info: $resolveInfo")
}

로그를 보면 아래처럼 모든 액티비티가 일치합니다. 인텐트에 *을 넣고 쿼리를 하면 모든 것을 처리할 수 있는 액티비티를 찾는 것이 아니라, 어떤 타입이 와도 상관없다라는 의미입니다. 따라서 audio/로 시작하는 인텐트필터는 모두 일치하게 됩니다.

MainActivity: uri: http://foo.com/apps
MainActivity: info: ResolveInfo{d6c2aa4 com.codechacha.intent/.SubActivity m=0x608000}
MainActivity: info: ResolveInfo{ae4500d com.codechacha.intent/.SubActivity2 m=0x608000}
MainActivity: info: ResolveInfo{aa83bc2 com.codechacha.intent/.SubActivity3 m=0x608000}

방금 MIME타입으로 인텐트필터를 설계할 때, 헷갈리는 것을 한가지 배웠습니다.

이제 다시 인텐트필터의 정의를 변경해주세요. 다른 케이스에 대해서 알아보겠습니다.

<activity android:name=".SubActivity">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="http" />
        <data android:mimeType="audio/mp3"/>
    </intent-filter>
</activity>
<activity android:name=".SubActivity2">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="content" />
        <data android:mimeType="audio/mp3"/>
    </intent-filter>
</activity>
<activity android:name=".SubActivity3">
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:mimeType="audio/mp3"/>
    </intent-filter>
</activity>

지금까지 배운 지식은, 인텐트필터에 Data와 MIME타입이 정의되어 있고, Data와 MIME타입을 갖고 있는 인텐트로 쿼리를 했을 때 두 속성 모두 일치하지 않으면 결과에 포함되지 않는다는 것이었습니다.

그럼 아래코드로 쿼리를 해볼까요? 인텐트는 데이터가 http://foo.com/apps로, MIME타입은 audio/mp3로 정의되어 있습니다. 위의 인텐트필터를 보고 결과를 예상해보면 SubActivity만 일치할 것 같습니다. 결과를 볼까요?

val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
val uri = Uri.parse("http://foo.com/apps")
val mimetype = "audio/mp3"
intent.setDataAndType(uri, mimetype)
intent.setPackage(packageName)

Log.d(TAG, "uri: ${uri.toString()}")
var results = packageManager.queryIntentActivities(intent, 0)
results?.forEach { resolveInfo ->
    Log.d(TAG, "info: $resolveInfo")
}

네.. 예상대로 SubActivity만 일치하였네요. 다른 것은 scheme이 content이거나 정의되지 않았습니다.

12-08 09:31:08.457  5752  5752 D MainActivity: uri: http://foo.com/apps
12-08 09:31:08.458  5752  5752 D MainActivity: info: ResolveInfo{8e50b55 com.codechacha.intent/.SubActivity m=0x608000}

그럼, 다른 것은 동일하고 데이터만 content://foo.com/apps로 변경하면 어떻게 될까요? 추측해보면 SubActivity2만 일치할 것 같습니다. 결과를 볼까요?

val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
val uri = Uri.parse("content://foo.com/apps")
val mimetype = "audio/mp3"
intent.setDataAndType(uri, mimetype)
intent.setPackage(packageName)

Log.d(TAG, "uri: ${uri.toString()}")
var results = packageManager.queryIntentActivities(intent, 0)
results?.forEach { resolveInfo ->
    Log.d(TAG, "info: $resolveInfo")
}

결과를 보면 SubActivity2와 SubActivity3이 일치하였습니다. 인텐트필터에 데이터를 설정하지 않은 SubActivity3는 왜 일치하였을까요?

12-08 09:26:51.524  5573  5573 D MainActivity: uri: content://foo.com/apps
12-08 09:26:51.526  5573  5573 D MainActivity: info: ResolveInfo{bcd0537 com.codechacha.intent/.SubActivity2 m=0x608000}
12-08 09:26:51.526  5573  5573 D MainActivity: info: ResolveInfo{d6c2aa4 com.codechacha.intent/.SubActivity3 m=0x608000}

Deverlopers를 유심히 보시면 예외에 대해서 나와 있습니다. 인텐트필터에 MIME타입은 정의하였지만 데이터를 정의하지 않은 경우 schemecontent이거나 file인 것으로 간주한다라고 되어있습니다. 따라서, 정의를 하지 않았기 떄문에 scheme이 content인 경우 일치하게 되었습니다.

그럼 데이터를 file://foo.com/apps로 변경하여 쿼리해보겠습니다. 결과를 미리 예상해보면 SubActivity3만 일치할 것 같습니다.

val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addCategory(Intent.CATEGORY_BROWSABLE)
val uri = Uri.parse("file://foo.com/apps")
val mimetype = "audio/mp3"
intent.setDataAndType(uri, mimetype)
intent.setPackage(packageName)

Log.d(TAG, "uri: ${uri.toString()}")
var results = packageManager.queryIntentActivities(intent, 0)
results?.forEach { resolveInfo ->
    Log.d(TAG, "info: $resolveInfo")
}

로그를 확인해보면 SubActivity3만 일치하였습니다.

12-08 09:36:34.171  5918  5918 D MainActivity: uri: file://foo.com/apps
12-08 09:36:34.171  5918  5918 D MainActivity: info: ResolveInfo{ee33236 com.codechacha.intent/.SubActivity3 m=0x608000}

정리

이전 글에서 Action, Category에 대해서 알아보았고, 이번 글에서 Data와 MIME타입에 대해서 알아보았습니다. 특히 Data와 MIME타입으로 인텐트필터를 설계할 때 생략해도 되는 부분이 있습니다. 예외사항을 기억하지 못하고 생략해버리면 의도와 다르게 동작하는 경우가 생길 수 있습니다. 생략보다는 모든 속성을 정의하는 것이 좋을 것 같습니다. 또한, 데이터에서 속성을 정의하거나 하지 않는 것의 의미가 다르기 때문에 이 부분도 유의해야합니다. 마지막으로 *은 인텐트필터에서 사용하는 것과 인텐트에서 사용하는 의미가 다르기 때문에 이를 인지하고 사용해야 합니다.

참고