Infinito Nirone 7

白羽の矢を刺すスタイル

OAuth 2.0 の Redirect URI で戻ってくる画面に Navigation Architecture Component の DeepLink を設定すると直面する問題と回避策

みなさん、やっていますか?私はやっています。

Slack API を使いたくて、Slack の OAuth の仕様通り Redirect URI を設定してアプリに auth codestate を戻すために Navigation Architecture Component を使った DeepLink の実装をしたところ、盛大にハマったので記録を残しておきます。

OAuth の Redirect URI みたいなものをさくっと再現した状況で試してみます (https://github.com/KeithYokoma/NavComponentDeepLinkSample)。

前準備

アプリケーションを2つ用意します。この記事では DeepLink で開くアプリを nav1、DeepLink を発動するアプリを nav2 とします。nav1 から nav2 を普通の Intent で開くボタンで nav2 に遷移し、nav2 から nav1 を DeepLink で開くボタンをそれぞれ持っているとします。

まず準備するものとして次のものを使えるようにしておきます。

  1. Naivgation Architecture Components (Navigation): 2.1.0

そして画面としては次のような構成にしておきます。

まず nav1 のほう。

  1. 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>
  1. 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}&amp;state={state}"/>
    </fragment>
</navigation>

簡単ですね。DeepLinkActivity があって、Slack の仕様にあるように Redirect URI のパラメータとして codestate を受け取れるようにします。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 を受け取ると codestateFragment#setArgumentsBundle に詰め込まれるので、次のコードのように、onResume 等で getArgumentsBundle をとってきて中身を確認知れば、codestate がいるはずです。

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) のドキュメントにあります。

developer.android.com

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) を呼び出す必要があります。