Android WebView から別アプリを開く

AndroidのWebViewでは`tel`や`mailto`などのリンクを踏んでも電話アプリやメールアプリが起動しないためそれに対応するための実装が必要です。  
また暗黙的Intentに対応する場合も、そのための実装が必要です。  
これらの実装方法を紹介します。

## URL判定処理

URLからIntentを作成します。

```kotlin
class WebViewRequestValidation {
    fun isTransitionToOtherApp(url: Uri): TransitionToOtherApp {
        return when (url.scheme) {
            "tel" -> TransitionToOtherApp.Yes(Intent(Intent.ACTION_DIAL, url))
            "mailto" -> TransitionToOtherApp.Yes(Intent(Intent.ACTION_SENDTO, url))
            "intent" -> TransitionToOtherApp.Yes(Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME))
            else -> TransitionToOtherApp.No()
        }
    }
    sealed class TransitionToOtherApp {
        class Yes(val intent: Intent):TransitionToOtherApp()
        class No():TransitionToOtherApp()
    }
}
```

## Intent起動処理

上記で作成したIntentを起動します。処理の流れは以下の通りです。

* 対象Activityが存在すれば、startActivityを実行する。
  * 存在しないActivityをstartActivityしてしまうと、アプリがクラッシュします。 
* intentから`browser_fallback_url`が取得できれば、そのURLを表示する。
  * こちらの処理について、詳しくは[Chromeのドキュメント](https://developer.chrome.com/multidevice/android/intents)を参照ください。 
* GooglePlayStoreアプリがアクティブであれば（存在すれば）、GooglePlayStoreアプリを起動する。
* 上記全てがヒットしない場合、`https://play.google.com/store/apps/details?id=$applicationId`を表示する。

最後の`https://play.google.com/store/apps/details?id=$applicationId`は、表示してもアプリをダウンロードできないので、なにかユーザにメッセージを表示するほうが適切かもしれません。


```kotlin
class ImplicitIntentStarter(private val wContext: WeakReference<Context>) {

    fun startActivity(intent: Intent) {
        val context = wContext.getNullable() ?: return
        val packageManager = context.packageManager

        if (intent.resolveActivity(packageManager) != null) {
            context.startActivity(intent)
            return
        }

        val fallbackUrl = intent.getStringExtra("browser_fallback_url")
        if (fallbackUrl != null) {
            // ブラウザ起動など
            return
        }

        startGooglePlayStoreActivity(intent.`package`)
    }

    fun startGooglePlayStoreActivity(applicationId: String) {
        val context = wContext.getNullable() ?: return
        val packageManager = context.packageManager

        val marketIntent = Intent(Intent.ACTION_VIEW).setData(Uri.parse("market://details?id=$applicationId"))
        if (marketIntent.resolveActivity(packageManager) != null) {
            context.startActivity(marketIntent)
            return
        }
        context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$applicationId")))
    }
}

// WeakReferenceから取得する値を null safe に扱うための拡張です。
fun <T> WeakReference<T>.getNullable(): T? = get()
```

## WebViewのリクエストをインターセプト

最後に、WebViewのリクエストをインターセプトし、上記の処理を実装します。

```kotlin
    private val validation = WebViewRequestValidation()
    private val implicitIntentStarter = ImplicitIntentStarter(wContext)
    val webViewClient = object : WebViewClient() {
        override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {

            // WebViewの画面遷移をさせたくない場合は true を返す。
            val allowedRequest = false
            val notAllowedRequest = true

            val urlString = url ?: return notAllowedRequest
            val url = Uri.parse(urlString)

            validation.isTransitionToOtherApp(url).let {
                when (it) {
                    is WebViewRequestValidation.TransitionToOtherApp.Yes -> {
                        implicitIntentStarter.startActivity(it.intent)
                        return notAllowedRequest
                    }
                    is WebViewRequestValidation.TransitionToOtherApp.No -> {}
                }
            }

            return allowedRequest

        }
    }
    
    webView.setWebViewClient(webViewClient)
```