みなさん、やっていますか?私はやっています。
Slack API を使いたくて、Slack の OAuth の仕様通り Redirect URI を設定してアプリに auth code と state を戻すために Navigation Architecture Component を使った DeepLink の実装をしたところ、盛大にハマったので記録を残しておきます。
OAuth の Redirect URI みたいなものをさくっと再現した状況で試してみます (https://github.com/KeithYokoma/NavComponentDeepLinkSample)。
前準備
アプリケーションを2つ用意します。この記事では DeepLink で開くアプリを nav1、DeepLink を発動するアプリを nav2 とします。nav1 から nav2 を普通の Intent で開くボタンで nav2 に遷移し、nav2 から nav1 を DeepLink で開くボタンをそれぞれ持っているとします。
まず準備するものとして次のものを使えるようにしておきます。
- Naivgation Architecture Components (Navigation):
2.1.0
そして画面としては次のような構成にしておきます。
まず nav1 のほう。
- AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="dev.keithyokoma.nav1"> <application> <activity android:name=".DeepLinkActivity" android:launchMode="singleTask"> <nav-graph android:value="@navigation/nav_graph"/> </activity> </application> </manifest>
- nav_graph.xml
<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/nav_graph" app:startDestination="@+id/nav_deep_link"> <fragment android:id="@+id/nav_deep_link" android:name="dev.keithyokoma.nav1.DeepLinkFragment" android:label="DeepLink"> <argument android:name="code" app:argType="string"/> <argument android:name="state" app:argType="string"/> <deepLink app:uri="nav1://deep/link?code={code}&state={state}"/> </fragment> </navigation>
簡単ですね。DeepLinkActivity があって、Slack の仕様にあるように Redirect URI のパラメータとして code と state を受け取れるようにします。DeepLink を受け取ると DeepLinkFragment を表示します。
OAuth 2.0 では code は auth token を得るために一時的に発行されるトークンで、Slack なら Slack が生成して渡してくれます。一方で state はアプリケーションが発行し、ID Provider (Slack など)に渡します。そして Redirect URI で返ってきた state をみて同一なら途中でよくないことが起きていないチェックができるというものです(ざっくり)。
ということは、nav1 にある DeepLinkFragment は自分がどんな state を発行したのか覚えておく必要があります。そのため、DeepLinkActivity には android:launchMode="singleTask" を設定しているわけです。これがないと、DeepLink を発動するたびにあたらしい DeepLinkActivity を起動してしまい、どんなに ViewModel などで覚えていても意味がなくなります。
DeepLink を発動してみる
ここで DeepLinkFragment で何をしているかを見てみます。
通常、DeepLink を受け取ると code や state は Fragment#setArguments の Bundle に詰め込まれるので、次のコードのように、onResume 等で getArguments で Bundle をとってきて中身を確認知れば、code や state がいるはずです。
class DeepLinkFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_deep_link, container, false).also { it.findViewById<Button>(R.id.button).setOnClickListener { startActivity(Intent() .setComponent(ComponentName("dev.keithyokoma.nav2", "dev.keithyokoma.nav2.MainActivity")) ) } } // ここで DeepLink のパラメータを読む override fun onResume() { super.onResume() arguments?.keySet()?.forEach { println(arguments?.get(it)) } } }
そして nav1 の DeepLinkFragment から nav2.MainActivity を立ち上げ、nav2.MainActivity にあるボタンで DeepLink を発動してみると…
println() で出てきてほしいログが何一つ出てきません。つらい!
なぜなのか
要点は NavController#handleDeepLink(Intent) のドキュメントにあります。
Checks the given Intent for a Navigation deep link and navigates to the deep link if present. This is called automatically for you the first time you set the graph if you've passed in an Activity as the context when constructing this NavController
このメソッドは DeepLink を受けとって Activity が起動すると、Navigation Graph を構築する段階で Graph 内にある適切な Fragment に DeepLink のパラメータを渡します。
つまり今回の実装のような、すでに Activity が起動していて (== すでに Navigation Graph が構築済の状態で)、その Activity を DeepLink で再度呼び戻す場合には自動で DeepLink のパラメータを渡してくれません。さっきのドキュメントには続きがあり、
but should be manually called if your Activity receives new Intents in Activity.onNewIntent(Intent).
とあるので、Activity#onNewIntent(Intent) を使って自分で NavController#handleDeepLink(Intent) を呼び出す必要があります。