LoginSignup
22
12

More than 1 year has passed since last update.

Intent / intent-filter のCategoryとはなんなのか?

Last updated at Posted at 2021-08-29

Androidを扱う上で基本中の基本であるIntentですが、様々なパラメータを利用します、Action / Category / Extra / Flag / Data(Uri) とありますが、Categoryってあまり意味を意識して使わないのではないでしょうか?
ここでは、Categoryってなんなのかを説明してみようと思います。

intent-filterとintent

Categoryがどのように扱われるのかを調べるため、サンプルアプリを作ります。

アプリA(com.android.myapplication2):単に起動されるアプリです。検証用にhogeというschemeを受け取れるintent-filterを設定しておきます。

AndroidManifest.xml
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="hoge" />
</intent-filter>

アプリB(com.android.myapplication):アプリAを起動するアプリです。
単にstartActivityするだけですが、同じIntentをqueryIntentActivitiesresolveActivityに渡した結果を表示するようにしてみます。

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 }

はい、queryIntentActivitiesresolveActivityではアプリAが見つかっていますが、startActivityはActivityNotFoundExceptionになってしまいました。

Intent.CATEGORY_DEFAULT

いきなり変な挙動を見せましたが、このような挙動になる理由は、暗黙的IntentはstartActivity時に自動的にIntent.CATEGORY_DEFAULTが付与されているため、intent-filterにandroid.intent.category.DEFAULTがついていないため反応しなかったのです。

なので、暗黙的Intentを受け取る場合は、intent-filterにandroid.intent.category.DEFAULTを付与します。

AndroidManifest.xml
<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

と、queryIntentActivitiesresolveActivityの結果は変わらず、起動に成功します。

逆に、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は通常追加しないと思います、queryIntentActivitiesresolveActivityで暗黙的IntentをstartActivityに渡した場合と同じ結果が必要な場合は、queryIntentActivitiesresolveActivityの第二引数である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です。

AndroidManifest.xml
<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_DEFAULTIntent.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にないため起動できません。

AndroidManifest.xml
<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に実装されています

IntentFilter.java
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になります。

IntentFilter.java
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を指定する

ということも分かりましたね。

以上です。

22
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
12