Androidを扱う上で基本中の基本であるIntentですが、様々なパラメータを利用します、Action / Category / Extra / Flag / Data(Uri) とありますが、Categoryってあまり意味を意識して使わないのではないでしょうか?
ここでは、Categoryってなんなのかを説明してみようと思います。
intent-filterとintent
Categoryがどのように扱われるのかを調べるため、サンプルアプリを作ります。
アプリA(com.android.myapplication2):単に起動されるアプリです。検証用にhogeというschemeを受け取れるintent-filterを設定しておきます。
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="hoge" />
</intent-filter>
アプリB(com.android.myapplication):アプリAを起動するアプリです。
単にstartActivity
するだけですが、同じIntentをqueryIntentActivities
とresolveActivity
に渡した結果を表示するようにしてみます。
val intent = Intent(Intent.ACTION_VIEW).also {
it.data = Uri.parse("hoge://test")
}
packageManager.queryIntentActivities(intent, 0).forEach {
Log.e("XXXX", "queryIntentActivities: ${it.activityInfo?.packageName}/${ it.activityInfo?.name}")
}
packageManager.resolveActivity(intent, 0).let {
Log.e("XXXX", "resolveActivity: ${it?.activityInfo?.packageName}/${ it?.activityInfo?.name}")
}
try {
startActivity(intent)
} catch (e: Exception) {
Log.e("XXXX", "error", e)
}
早速一発実行してみましょう。
E/XXXX: queryIntentActivities: com.android.myapplication2/com.android.myapplication2.MainActivity
E/XXXX: resolveActivity: com.android.myapplication2/com.android.myapplication2.MainActivity
E/XXXX: error
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat=hoge://test }
はい、queryIntentActivities
とresolveActivity
ではアプリAが見つかっていますが、startActivity
はActivityNotFoundExceptionになってしまいました。
Intent.CATEGORY_DEFAULT
いきなり変な挙動を見せましたが、このような挙動になる理由は、暗黙的IntentはstartActivity時に自動的にIntent.CATEGORY_DEFAULT
が付与されているため、intent-filterにandroid.intent.category.DEFAULT
がついていないため反応しなかったのです。
なので、暗黙的Intentを受け取る場合は、intent-filterにandroid.intent.category.DEFAULT
を付与します。
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="hoge" />
</intent-filter>
これで実行すると、
E/XXXX: queryIntentActivities: com.android.myapplication2/com.android.myapplication2.MainActivity
E/XXXX: resolveActivity: com.android.myapplication2/com.android.myapplication2.MainActivity
と、queryIntentActivities
とresolveActivity
の結果は変わらず、起動に成功します。
逆に、intent-filterを逆の状態で、intentにIntent.CATEGORY_DEFAULT
を追加してみます。
val intent = Intent(Intent.ACTION_VIEW).also {
it.data = Uri.parse("hoge://test")
it.addCategory(Intent.CATEGORY_DEFAULT)
}
すると、結果は以下のようになり、queryIntentActivities
は空、resolveActivity
はnullが返ってくるようになり、startActivityの結果と一致します。
E/XXXX: resolveActivity: null/null
E/XXXX: error
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW cat=[android.intent.category.DEFAULT] dat=hoge://test }
ただ、Intent.CATEGORY_DEFAULT
は通常追加しないと思います、queryIntentActivities
とresolveActivity
で暗黙的IntentをstartActivityに渡した場合と同じ結果が必要な場合は、queryIntentActivities
とresolveActivity
の第二引数であるflagsにPackageManager.MATCH_DEFAULT_ONLY
を指定します。
val intent = Intent(Intent.ACTION_VIEW).also {
it.data = Uri.parse("hoge://test")
}
packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).forEach {
Log.e("XXXX", "queryIntentActivities: ${it.activityInfo?.packageName}/${ it.activityInfo?.name}")
}
packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY).let {
Log.e("XXXX", "resolveActivity: ${it?.activityInfo?.packageName}/${ it?.activityInfo?.name}")
}
これで先ほどと同じ結果を得ることができます。
このことから
IntentにCategoryがついている場合、intent-filterにもついていなければ合致しない
と言えるでしょう。
Intent.CATEGORY_DEFAULTのつかないintent-filter
ちょっと話が横道にそれますが、暗黙的Intentを受け取るにはIntent.CATEGORY_DEFAULT
が必要です。明示的intentであればComponentNameが指定されるのでintent-filterが無くてもIntentを受け取ることができます。では、Intent.CATEGORY_DEFAULT
がついていないintent-filterって意味があるのかな?と思ってしまったりするかもしれませんが、身近な例がありますね。
テンプレートから新規プロジェクトを作成すると作成されるintent-filterです。
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
起動Acitivtyについているintent-filterです。
これがどういう意味合いかは以下の記事で説明していますが、ホームアプリから起動起点となるActivityを探すために利用されます。
超シンプルなホームアプリを作る ~ホームアプリ(ランチャーアプリ)の作り方~
簡単にいうと、Intent.CATEGORY_LAUNCHER
がついたintent-filterを検索し、そのComponentNameを利用して明示的Intentを投げます。つまり、Intentを直接受け取るために使用されるわけではなく、intent-filterを検索するために利用されています。当然、この目的で検索をするときはPackageManager.MATCH_DEFAULT_ONLY
は使いません。
複数のCategoryを追加してみる
何でも良いので、Intent.CATEGORY_BROWSABLEを追加してみましょう。
val intent = Intent(Intent.ACTION_VIEW).also {
it.data = Uri.parse("hoge://test")
it.addCategory(Intent.CATEGORY_BROWSABLE)
}
なお、暗黙的IntentはstartActivity時にIntent.CATEGORY_DEFAULTが自動で付与されるというのは、他のCategoryがすでに設定されている場合でも同様です。つまり、上記intentはstartActivity時にはIntent.CATEGORY_DEFAULT
とIntent.CATEGORY_BROWSABLE
が付与された状態になります。
それでは実行!
E/XXXX: resolveActivity: null/null
E/XXXX: error
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW cat=[android.intent.category.BROWSABLE] dat=hoge://test }
はい、やはり、IntentにIntent.CATEGORY_BROWSABLE
がついているのにintent-filterにないため起動できません。
<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="hoge" />
</intent-filter>
こうすれば、成功します。
queryIntentActivities: com.android.myapplication2/com.android.myapplication2.MainActivity
resolveActivity: com.android.myapplication2/com.android.myapplication2.MainActivity
逆に、Intentの方のCategoryを削除してみます
val intent = Intent(Intent.ACTION_VIEW).also {
it.data = Uri.parse("hoge://test")
}
これは、成功します。
queryIntentActivities: com.android.myapplication2/com.android.myapplication2.MainActivity
resolveActivity: com.android.myapplication2/com.android.myapplication2.MainActivity
このことから、
intent-filterに設定されているCategoryはIntentに付与されていなくても合致する
と言えるでしょう。
intentとintent-filterの合致アルゴリズム
intentとintent-filterが合致するかのロジックは、IntentFilter#matchに実装されています
public final int match(String action, String type, String scheme,
Uri data, Set<String> categories, String logTag, boolean supportWildcards,
@Nullable Collection<String> ignoreActions) {
if (action != null && !matchAction(action, supportWildcards, ignoreActions)) {
if (false) Log.v(
logTag, "No matching action " + action + " for " + this);
return NO_MATCH_ACTION;
}
int dataMatch = matchData(type, scheme, data, supportWildcards);
if (dataMatch < 0) {
if (false) {
if (dataMatch == NO_MATCH_TYPE) {
Log.v(logTag, "No matching type " + type
+ " for " + this);
}
if (dataMatch == NO_MATCH_DATA) {
Log.v(logTag, "No matching scheme/path " + data
+ " for " + this);
}
}
return dataMatch;
}
String categoryMismatch = matchCategories(categories);
if (categoryMismatch != null) {
if (false) {
Log.v(logTag, "No matching category " + categoryMismatch + " for " + this);
}
return NO_MATCH_CATEGORY;
}
// It would be nice to treat container activities as more
// important than ones that can be embedded, but this is not the way...
if (false) {
if (categories != null) {
dataMatch -= mCategories.size() - categories.size();
}
}
return dataMatch;
}
簡単にみると、Actionを確認し、Dataを確認し、次にcategoryを確認していますね。
matchCategoriesの中身を見てみましょう。戻り値がStringで分かりにくいですが、合致しなかったCategoryが戻り、合致した場合はnullになります。
public final String matchCategories(Set<String> categories) {
if (categories == null) {
return null;
}
Iterator<String> it = categories.iterator();
if (mCategories == null) {
return it.hasNext() ? it.next() : null;
}
while (it.hasNext()) {
final String category = it.next();
if (!mCategories.contains(category)) {
return category;
}
}
return null;
}
ここからも、intentに付与されているCategoryがすべてintent-filterに含まれている場合、合致、intent-filter側にあるが、intent側にないものは無視されていることが分かります。
余談ですが、上記IntentFilterのソースコードは一読をお勧めします。
まとめ
ということでまとめると
- intentに付与したCategoryがすべてintent-filterに記載されていないと、合致しない
- intent-filterに付与したCategoryがintentに付与されていなくても、合致する
というルールになっていて、intentでintent-filterのカテゴリーを指定するために使うものであると言えるでしょう。intent-filterからintentのカテゴリーを指定するためには使用できません。
また、おまけ的にですが、
- 暗黙的intentを受け取るにはintent-filterに
Intent.CATEGORY_DEFAULT
を付与する必要がある - 暗黙的intentを投げた場合と同じ結果を得るためには、
queryIntentActivities
などのflagsにPackageManager.MATCH_DEFAULT_ONLY
を指定する
ということも分かりましたね。
以上です。