概要
Android の基礎的な仕組みである Intent について整理します。 下記のページの内容を私なりに整理したものです。
解説
コンポーネント
Intent はコンポーネント間の通知に使われる。 Intent の日本語訳は、意図や意思。 Intent はコンポーネント間を伝わる意思ということ。
Android におけるコンポーネントは下記の 4 種類 (アプリケーションの基礎 | Android Developers 参照)。
- アクティビティ (Activity)
- サービス (Service)
- ブロードキャストレシーバー (BroadcastReceiver)
- コンテンツプロバイダー (ContentProvider)
ContentProvider を除く 3 種類のコンポーネントは Intent によりアクティベート(起動、機能を有効化)される。 本記事では便宜上、Intent によりアクティベートされる 3 種類をコンポーネント、アクティベートを起動と呼ぶ。
コンポーネントを起動する
コンポーネント (Activity, Service, BroadcastReceiver) を起動するメソッドは下記のとおり。
- Activity
- startActivity
- startActivityForResult : Activity から結果を受け取りたい場合に使用
- Service
- startService
- bindService : Service と接続して通信したい場合に使用
- BroadcastReceiver
- sendBroadcast
- sendOrderedBroadcast
- sendStickyBroadcast
各メソッドは引数に Intent を受け取る。 Intent によって起動するコンポーネントを指定する。
Intent オブジェクトの生成
Intent クラスのコンストラクタは下記の 6 つ。
- Intent()
- 万能
- Intent(Intent o)
- Intent オブジェクトのコピー
- Intent(String action)
- Intent(String action, Uri uri)
- Intent(Context packageContext, Class<?> cls)
- Intent(String action, Uri uri, Context packageContext, Class<?> cls)
最初の 2 つを除くコンストラクタを分類すると 3 通りになる。
- Context packageContext, Class<?> cls を引数にとるコンストラクタ
- 明示的 Intent として生成する
- String action を引数にとるコンストラクタ
- アクションを指定して生成
- Uri uri を引数にとるコンストラクタ
- データの URI を指定して生成
明示的 Intent としての指定、アクション指定、データの URI 指定はコンストラクタ以外でも指定可能である。
Intent に指定する情報
- コンポーネント名
- クラスの完全修飾名 (例えば、"com.example.project" + "PingPongActivity")
- コンストラクタの cls で指定
- setComponent、setClass、setClassName メソッドで指定
- 指定すると明示的 Intent として扱われる
- アクション
- 実行する内容を意味する文字列 ("パッケージ名.アクション名" とする。例えば、"com.example.project.ACTION_FLY")
- 一般的なアクションは定義済み (Intent | Android Developers 参照)
- コンストラクタの action で指定
- setAction メソッドで指定
- データ
- カテゴリ
- コンポーネントの種類を表す文字列 ("パッケージ名.カテゴリ名" とする。例えば、"android.intent.category.LAUNCHER")
- 一般的なカテゴリは定義済み (Intent | Android Developers 参照)
- addCategory メソッドで指定
- startActivity, startActivityForResult では、Intent に "android.intent.category.DEFAULT" が自動的に指定される
- エクストラ
- 任意の追加情報 (キーと値のペア)
- アクション毎に指定できるキー(と値の型)が決まっている
- キーは "パッケージ名.キー名" とする (例えば、"com.example.project.EXTRA_INTO_RIVER")
- 一般的なキーは定義済み (Intent | Android Developers 参照)
- putExtra, putExtras, put[Type]ArrayListExtra メソッドで指定
- 任意の追加情報 (キーと値のペア)
- フラグ
- メタデータ
- addFlags, setFlags で指定
Intent のフラグ
startActivity, startActivityForResult に渡す Intent で指定するフラグ
- FLAG_ACTIVITY_BROUGHT_TO_FRONT
- FLAG_ACTIVITY_CLEAR_TASK
- FLAG_ACTIVITY_CLEAR_TOP
- FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
- FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
- FLAG_ACTIVITY_FORWARD_RESULT
- FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
- FLAG_ACTIVITY_MULTIPLE_TASK
- FLAG_ACTIVITY_NEW_DOCUMENT
- FLAG_ACTIVITY_NEW_TASK
- FLAG_ACTIVITY_NO_ANIMATION
- FLAG_ACTIVITY_NO_HISTORY
- FLAG_ACTIVITY_NO_USER_ACTION
- FLAG_ACTIVITY_PREVIOUS_IS_TOP
- FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
- FLAG_ACTIVITY_REORDER_TO_FRONT
- FLAG_ACTIVITY_SINGLE_TOP
- FLAG_ACTIVITY_TASK_ON_HOME
sendBroadcast, sendOrderedBroadcast, sendStickyBroadcast に渡す Intent で指定するフラグ
- FLAG_RECEIVER_REGISTERED_ONLY
共通のフラグ?
- FLAG_GRANT_READ_URI_PERMISSION
- FLAG_GRANT_WRITE_URI_PERMISSION
- FLAG_GRANT_PERSISTABLE_URI_PERMISSION
- FLAG_GRANT_PREFIX_URI_PERMISSION
- FLAG_DEBUG_LOG_RESOLUTION
- FLAG_FROM_BACKGROUND
Intent の種類
明示的 Intent
コンポーネントが指定された Intent は明示的 Intent となる。 明示的 Intent では指定したコンポーネントが起動される。
暗黙的 Intent
コンポーネントが指定されていない Intent は暗黙的 Intent となる。 Intent に指定したアクション、データ、カテゴリと各コンポーネントの intent-filter でマッチングを行い、起動するコンポーネントを実行時に決定する。 マッチングを行った結果、コンポーネントが見つからない場合や複数見つかる場合がある。
Service では暗黙的 Intent を使用せず、intent-filter を定義しないこと。 Service で暗黙的 Intent と intent-filter を使用すると、ユーザーは使用される Service を把握できず、セキュリティ上の危険を伴う。 bindService メソッドに暗黙的 Intent を渡すと例外がスローされる (API Level ≧ 21)。
コンポーネントが見つからない場合
startActivity, startActivityForResult を呼び出し、コンポーネントが見つからなかった場合にはアプリがクラッシュする。 クラッシュしないようにするために resolveActivity メソッドにより、Intent に応答できるコンポーネントの存在を確認する。
val sendIntent = Intent(Intent.ACTION_SEND) ... if (sendIntent.resolveActivity(packageManager) != null) { startActivity(sendIntent) }
Service は暗黙的 Intent では使用されず、BroadcastReceiver ではいずれの BroadcastReceiver も通知を受け取らない。
コンポーネントが複数見つかる場合
startActivity, startActivityForResult を呼び出し、コンポーネントが複数見つかった場合には下記のいずれかの振る舞いをする。
- アプリチューザ (アプリ選択ダイアログ) が表示され、ユーザーに Activity の選択を促す
- ユーザーがアクションに対して選択したデフォルトの Activity が選択される
アクションに対するデフォルトの Activity が選択されている場合でも、強制的にアプリチューザを表示することができる。
val sendIntent = Intent(Intent.ACTION_SEND) ... val chooser = Intent.createChooser(sendIntent, "Chooser Title"); if (sendIntent.resolveActivity(packageManager) != null) { startActivity(chooser) }
Service は暗黙的 Intent では使用されず、BroadcastReceiver ではマッチングに成功した全ての BroadcastReceiver が通知を受けとる。
マッチングの仕組み
AndroidManifest.xml の各コンポーネント(activity, receiver 要素)に intent-filter 要素を追加することで、各コンポーネントが受信する Intent を指定する。 BroadcastReceiver の intent-filter は Context クラスの registerReceiver, unregisterReceiver メソッドで動的に変更できる。 動的に変更することでアプリ実行中のみ受信できる。 Service は暗黙的 Intent では使用しないため、intent-filter 要素を追加しないこと。
intent-filter 要素は各コンポーネント(activity, receiver 要素)に複数指定できる。 複数指定した場合は or 条件として、Intent がいずれかの intent-filter にマッチした場合にコンポーネントが起動される。
intent-filter 要素には、action, category, data 要素を含むことができる。 各要素は intent-filter 要素内に複数指定できる。
<activity android:name="PingPongActivity"> <intent-filter> <action android:name="android.intent.action.SEND"/> <action android:name="com.example.project.ACTION_FLY"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.LAUNCHER" /> <data android:mimeType="image/*"/> <data android:mimeType="video/*" android:scheme="http"/> <data android:scheme="content" android:host="project.example.com" android:port="200" android:path="folder/subfolder/etc"/> </intent-filter> </activity>
action, category, data 要素のマッチングは複雑であり、文章で説明すると曖昧になるため擬似コードでの整理を試みる。
intent match filter ::= ( ( intent.action == filter.action[0] or intent.action == filter.action[1] or ... ) and intent.category[0] match filter.category and intent.category[1] match filter.category and ... and ( intent.data match filter.data[0] or intent.data match filter.data[1] or ... ) ) intent.category[i] match filter.category ::= ( intent.category[i] == filter.category[0] or intent.category[i] == filter.category[1] or ... ) intent.data match filter.data[i] ::= ( ( intent.data.mimeType == null and filter.data[i].mimeType == null or intent.data.mimeType != null and intent.data.mimeType match filter.data[i].mimeType ) and ( intent.data.uri == null and filter.data[i].uri == null or intent.data.uri != null and intent.data.uri match filter.data[i].uri ) ) intent.data.uri match filter.data[i].uri ::= ( (filter.data[i].uri.scheme == null or intent.data.uri.scheme == filter.data[i].uri.scheme) and (filter.data[i].uri.host == null or intent.data.uri.host == filter.data[i].uri.host) and (filter.data[i].uri.port == null or intent.data.uri.port == filter.data[i].uri.port) and (filter.data[i].uri.path == null or intent.data.uri.path match filter.data[i].uri.path) )
intent-filter の補足事項。
data 要素の path, mimeType 属性では、* (ワイルドカード) 指定が可能。 部分一致が可能なので、上記の擬似コードでは == ではなく match としている。
data 要素の scheme, host, port, path 属性は下記に示す組み合わせのみ有効。
data に mimeType 属性のみが指定されている場合、scheme 属性に content, file が指定されているとして扱われる。 つまり、下記の 2 つは同じ。
<data mimeType="text/plain">
<data mimeType="text/plain" scheme="content"> <data mimeType="text/plain" scheme="file">
Intent の補足事項。
startActivity, startActivityForResult では Intent に CATEGORY_DEFAULT が追加されるので、 activity の intent-filter の category に CATEGORY_DEFAULT を指定する必要がある。
Intent で MIME タイプ (mimeType) を指定していなくても、data の URI から推測されて指定されることがある。
コンポーネントを探す
Intent を受け入れることができるコンポーネントは PackageManager クラスの下記のメソッドで探せる。
- queryIntentActivities(Intent intent, int flags)
- queryIntentServices(Intent intent, int flags)
- queryBroadcastReceivers(Intent intent, int flags)
Intent に応答できる最適なコンポーネントは PackageManager クラスの下記のメソッドで探せる。 (最適とは?)
- resolveActivity (Intent intent, int flags)
- resolveService (Intent intent, int flags)
PendingIntent
別のアプリケーションに Intent を実行させる場合には PendingIntent を使う。 別のアプリケーションの代表例は、通知、ウィジェット、アラーム。
PendingIntent は下記のメソッドにより取得する。
- Activity
- PendingIntent.getActivity
- Service
- PendingIntent.getService
- Broadcast
- PendingIntent.getBroadcast
使用例。
val intent = Intent(this, MainActivity::class.java) val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) val notification = NotificationCompat.Builder(context).apply { setContentIntent(pendingIntent) ... }.build()
補足
マッチングの仕組みの導出
コードでの整理に辿り着くまでの過程。
アクションのテストから。
Intent で指定されたアクションが、フィルタにリストされたアクションのいずれかに一致する必要があります。(*1)
フィルタのリストにアクションが 1 つもない場合は、インテントに一致するものがないため、すべてのインテントがテストに失敗します。(*2)
Intent がアクションを指定していない場合は、テストに合格します(フィルタに少なくとも 1 つのアクションが含まれている必要があります)。(*3)
filter.action | intent.action | match | 引用との対応 |
---|---|---|---|
null | null | false | *3, 実際に試すとダメ |
null | not null | false | *2 |
not null | null | false | *1 |
not null | not null | いずれかに一致 | *1 |
カテゴリのテストから。
インテントがカテゴリのテストに合格するには、Intent 内のすべてのカテゴリが、フィルタ内のカテゴリに一致する必要があります。
カテゴリのないインテントは、フィルタで宣言されているカテゴリに関係なく、常にこのテストに合格することになります。
(intent.category[0] ...) and (intent.category[1] ...)。
インテント フィルタでは、Intent で指定されたカテゴリよりも多くのカテゴリが宣言されている場合もあるため、すべてが Intent に一致しなくてもテストには合格します。
(intent.category[0] == filter.category[0]) or (intent.category[0] == filter.category[1])。
検証。
filter.category | intent.category | match |
---|---|---|
null | null | true |
null | A | false |
A | null | true |
A | A | true |
A | A,B | false |
データのテストから。
スキームが指定されていない場合、ホストは無視されます。
ホストが指定されていない場合、ポートは無視されます。
スキームとホストの両方が指定されていない場合、パスは無視されます。
全ての組み合わせを網羅して、無視される部分を () で囲むと
scheme, host, port, path scheme, host, port scheme, host, path scheme, host scheme, (port), path scheme, (port) scheme, path scheme (host), (port), (path) (host), (port) (host), (path) (host) (port), (path) (port) (path)
無視される場合は指定されていないことになると解釈している。 無視される場合を整理すると
scheme, host, port, path scheme, host, port scheme, host, path scheme, host scheme, path scheme
インテントの URI をフィルタの URI 仕様に比較するときは、フィルタに含まれる URI の一部でのみ比較されます。
フィルタでスキームのみが指定されている場合、そのスキームを持つすべての URI がフィルタに一致します。
フィルタでスキームと認証局が指定されていて、パスが指定されていない場合、パスにかかわらず同じスキームと認証局を持つすべての URI がフィルタを通過します。
フィルタでスキーム、認証局、パスが指定されている場合、同じスキーム、認証局、パスを持つ URI のみがフィルタを通過します。
intent.data = content::example.com:200/folder/subfolder として検証。
scheme | host | port | path | match |
---|---|---|---|---|
content | example.com | 200 | /folder/subfolder | true |
content | example.com | 200 | null | true |
content | example.com | null | /folder/subfolder | true |
content | example.com | null | null | true |
content | null | 200 | /folder/subfolder | IDE error |
content | null | 200 | null | IDE error |
content | null | null | /folder/subfolder | IDE error |
content | null | null | null | true |
null | example.com | 200 | /folder/subfolder | IDE error |
null | example.com | 200 | null | IDE error |
null | example.com | null | /folder/subfolder | IDE error |
null | example.com | null | null | IDE error |
null | null | 200 | /folder/subfolder | IDE error |
null | null | 200 | null | IDE error |
null | null | null | /folder/subfolder | IDE error |
先の整理した内容と比較すると scheme, path の組み合わせのみ期待と異なる。
scheme, host, port, path scheme, host, port scheme, host, path scheme, host scheme, path // 指定できない scheme
intent-filter で指定した属性が 1 つでも一致しなかった場合には、data 要素全体として一致しなかったことになる。
intent.data = content::example.com:200/folder/subfolder として検証。
scheme | host | port | path | match |
---|---|---|---|---|
content | example.com | 200 | /mismatch | false |
content | example.com | 80 | /folder/subfolder | false |
content | mismatch.com | 200 | /folder/subfolder | false |
http | example.com | 200 | /folder/subfolder | false |
URI も MIME タイプも含まないインテントは、フィルタで URI や MIME タイプが指定されていない場合のみテストをパスします。
URI を含んでいて MIME タイプを含んでいないインテント(明示的にも含まれておらず、URI からも推測できない)場合は、URI がフィルタの URI 形式に一致し、フィルタが MIME タイプを指定していない場合のみテストをパスします。
MIME タイプを含んでいて、URI を含んでいないインテントは、フィルタのリストに同じ MIME タイプがあり、URI 形式が指定されていない場合のみテストをパスします。
URI と MINE タイプの両方を含む(明示的か、URI からの推測)インテントは、MIME タイプがフィルタのリストにあるタイプに一致した場合のみ、テストの MIME タイプのパートをパスします。 テストの URI のパートは、URI がフィルタの URI に一致するか、content: URI か file: URI があって URI が指定されていない場合にパスできます。つまり、フィルタにリストに MIME タイプのみがある場合、コンポーネントは content: データと file: データをサポートすると推定されます。
次の条件で検証。
- intent
- uri = "http://example.com:200/folder/subfolder"
- mimeType = "text/plain"
- filter
- uri = scheme="http", host="example.com", port="200", path="/folder/subfolder"
- mimeType = "text/plain"
結果。
filter.mimeType | filter.uri | intent.mimeType | intent.uri | match |
---|---|---|---|---|
not null | not null | not null | not null | 共に一致すれば |
not null | not null | not null | null | false |
not null | not null | null | not null | false |
not null | null | not null | not null | false |
null | not null | not null | not null | false |
更に別の条件で検証。
- intent
- uri = "content://example.com:200/folder/subfolder"
- mimeType = "text/plain"
- filter
- uri = scheme="content", host="example.com", port="200", path="/folder/subfolder"
- mimeType = "text/plain"
結果。
filter.mimeType | filter.uri | intent.mimeType | intent.uri | match |
---|---|---|---|---|
not null | not null | not null | not null | 共に一致すれば |
not null | not null | not null | null | false |
not null | not null | null | not null | false |
not null | null | not null | not null | true |
null | not null | not null | not null | false |
filter.uri を省略した場合は、scheme="content" と scheme="file" が指定されている扱いとなる。