WebViewでintent://~をハンドリング

WebViewで外部サイトを表示するようなアプリを作成していると、ページ上の「アプリで見る」みたいなボタンを押すとintent://~というリンクが投げられてくることがある事に気づくと思います。
Chromeアプリなどでそこをタッチするとそのサイトのアプリ版がインストールされていればアプリが開き、インストールされていなければPlay Storeアプリが開いてそのアプリのページに飛ばされます。
これを自作WebViewで実現しようとしたらハマりにハマったので備忘録。

# 解決法
まずは結論から。
WebViewClientCompatクラスのshouldOverrideUrlLoadingメソッドの中で [Intent.URI_INTENT_SCHEME](https://developer.android.com/reference/android/content/Intent#URI_INTENT_SCHEME)を使用してパースしてやる。

```kotlin
 val webViewClient = object : WebViewClientCompat() {
override fun shouldOverrideUrlLoading(
                    view: WebView,
                    request: WebResourceRequest
            ): Boolean {
               

                if (request.url?.scheme.equals("intent") || request.url?.scheme.equals("android-app")) {
                    // android-appスキームもintentスキームの一種扱いらしく、Intent.URI_INTENT_SCHEMEでパース出来る。
                    // 以下のリンクでテスト済み。
                    // android-app://com.google.android.youtube/http/www.youtube.com/watch?v=dQw4w9WgXcQ
                    // android-appスキーム用のURI_ANDROID_APP_SCHEMEフラグも用意されているがAPIレベル22からなのでIntent.URI_INTENT_SCHEMEフラグで一緒にパースしちゃう。
                    // https://developer.android.com/reference/android/content/Intent#URI_INTENT_SCHEME
                    // https://developer.android.com/reference/android/content/Intent#URI_ANDROID_APP_SCHEME
                    val intent = Intent.parseUri(request.url.toString(), Intent.URI_INTENT_SCHEME)
                    val packageManager = activity?.packageManager

                    if (packageManager != null && intent?.resolveActivity(packageManager) != null) {
                        startActivity(intent)
                        return true
                    }

                }
                return false
            }
}

binding.webView.webViewClient = webViewClient
binding.webView.loadUrl(url)
```

※また、これが基本でこのままでちゃんと動くが、安全性を考えると実はこれだけでは不十分。詳しくは最後の「追記 : より安全性を高めるために」を参照。

# 解説
## intent://~とは何か。
アプリがインストールされていたらそれを起動し、インストールされていなかったらplay storeアプリを開いてダウンロードさせたりするときに使われるgoogle発のリンクの形式。
仕様など詳しくは[こちら](https://developer.chrome.com/multidevice/android/intents)

## 何をしているか
intent://~形式のリンクは、[Intent.URI_INTENT_SCHEME](https://developer.android.com/reference/android/content/Intent#URI_INTENT_SCHEME)とIntent.parseUrlメソッドを使用すると上記のリンクの仕様にのっとった適切なIntentを作ってくれる。それを利用してIntentを作って外に投げている。これで作成されたIntentはアプリがインストールされていればアプリを開き、なければplay storeアプリを開いてくれる。

## android-app;//~について
解決法のコードの中にも書いてあるが、android-app://~形式のリンクも実はintent://~の一種らしい。ので同じ方法でパース出来る。
本当android-app://~専用のフラグ[URI_ANDROID_APP_SCHEME](https://developer.android.com/reference/android/content/Intent#URI_ANDROID_APP_SCHEME)も用意されているのでIntent.URI_INTENT_SCHEMEではなくそちらを使った方がよいのだろうが、URI_ANDROID_APP_SCHEMEはAPIレベル22から入っているフラグで、今回のアプリはminSDKVersionがそれ未満だったのでIntent.URI_INTENT_SCHEMEでパースしてしまっている。

## ちなみに
このリンクの[仕様]([こちら](https://developer.chrome.com/multidevice/android/intents)
)にのっとって自力でパースしてIntentをつくることすることも可能とは思いますがものすごく手間だしパースできる機構が用意されているのだからそちらを使う方がいいと思います。(自分は最初この機構に気づいておらず自力でパースするコードを作ってしまっていました。半日以上かかりました;)


## 追記 : 自分でハンドリングするようにするとこんな感じ

まず、どこかに以下のようなメソッドを定義してやって

```kotlin
 fun parseFragment(urlFragment : String,regularEpressionValue : String,startCharNum : Int,endCharNum:Int):String?{
                val pattern = Pattern.compile(regularEpressionValue)
                val matcher = pattern.matcher(urlFragment)
                val list = ArrayList<String>()
                while (matcher.find()) {
                    val p = matcher.group()
                    if (p.length <= 9){
                        break
                    }
                    list.add(p.substring(startCharNum,p.length -endCharNum))
                }

                if (list.isEmpty()){
                    return null
                }else{
                    return list[0]
                }
            }

```

WebViewClientCompatクラスのshouldOverrideUrlLoadingメソッドの中で以下のようにすると一応自力でハンドリングもできる。(これはintent://~のみで、android-app://~形式は未対応ver。android-app://~形式も規格に沿ってパースしてやればうまくハンドリングできるはず。)



```kotlin

 if(request.url?.scheme.equals("intent") ) {
                    val urlFragment = request.url?.fragment!!
                    val hostValue = request.url.host
                    val schemeValue = parseFragment(urlFragment,"scheme=.+?;",7,1)
                    val packageValue = parseFragment(urlFragment,"package=.+?;",8,1)
                    val browserFallbackUrlValue = parseFragment(urlFragment,"S.browser_fallback_url=.+?;",23,1)
                    if (hostValue!=null && schemeValue != null){
                        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(schemeValue+"://"+hostValue+request.url.path+"?" +request.url.query))
//                        intent.setPackage(packageValue)
                        val packageManager = activity?.packageManager
                        if (packageManager != null && intent.resolveActivity(packageManager) != null) {
                            startActivity(intent)
                            return true
                        }
                    }
                    if (browserFallbackUrlValue!=null){
                        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(browserFallbackUrlValue))
                        val packageManager = activity?.packageManager
                        if (packageManager != null && intent.resolveActivity(packageManager) != null) {
                            startActivity(intent)
                            return true
                        }
                    }
                    if (packageValue!=null){

                        val packageManager = activity?.packageManager
                        val intent = Intent(Intent.ACTION_VIEW)
                        intent.setPackage(packageValue)

                        if (packageManager != null && intent?.resolveActivity(packageManager) != null) {
                            startActivity(intent)
                            return true
                        }
                    }
                    

                    return true


                }
```


# 追記 : より安全性を高めるために
```Intent.parseUri(request.url.toString(), Intent.URI_INTENT_SCHEME)```で生成したIntentをそのまま使うと、アプリのアプリケーションIdと同じ名前空間を持つActivityが存在する場合、それを開けるリンクを作られてしまう可能性がある。(https://www.m3tech.blog/entry/android-webview-intent-scheme)
なので、chromeアプリを参考に

```kotlin
 val intent = Intent.parseUri(request.url.toString(), Intent.URI_INTENT_SCHEME)

 // パースしたのをそのまま使うとアプリ内のActivityを開かれてしまう可能性があるので対策
 // https://www.m3tech.blog/entry/android-webview-intent-scheme
 intent.flags = intent.flags and ALLOWED_INTENT_FLAGS
 intent.addCategory(Intent.CATEGORY_BROWSABLE)
 // categoryなどのfieldにセットされた値を用いてどのクラスを開くか決める。
 // (setComponentで特定のクラスを指定してあると、fieldにセットされた値はまるで無視でそのクラスを開いてしまう)
 intent.component = null
 val selector = intent.selector
 if (selector != null) {
     selector.addCategory(Intent.CATEGORY_BROWSABLE)
     // categoryなどのfieldにセットされた値を用いてどのクラスを開くか決める。
     // (setComponentで特定のクラスを指定してあると、fieldにセットされた値はまるで無視でそのクラスを開いてしまう)
     selector.component = null
}

```

と加工してやる。