WebViewで外部サイトを表示するようなアプリを作成していると、ページ上の「アプリで見る」みたいなボタンを押すとintent://~というリンクが投げられてくることがある事に気づくと思います。
Chromeアプリなどでそこをタッチするとそのサイトのアプリ版がインストールされていればアプリが開き、インストールされていなければPlay Storeアプリが開いてそのアプリのページに飛ばされます。
これを自作WebViewで実現しようとしたらハマりにハマったので備忘録。
解決法
まずは結論から。
WebViewClientCompatクラスのshouldOverrideUrlLoadingメソッドの中で Intent.URI_INTENT_SCHEMEを使用してパースしてやる。
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発のリンクの形式。
仕様など詳しくはこちら
何をしているか
intent://~形式のリンクは、Intent.URI_INTENT_SCHEMEとIntent.parseUrlメソッドを使用すると上記のリンクの仕様にのっとった適切なIntentを作ってくれる。それを利用してIntentを作って外に投げている。これで作成されたIntentはアプリがインストールされていればアプリを開き、なければplay storeアプリを開いてくれる。
android-app;//~について
解決法のコードの中にも書いてあるが、android-app://~形式のリンクも実はintent://~の一種らしい。ので同じ方法でパース出来る。
本当android-app://~専用のフラグURI_ANDROID_APP_SCHEMEも用意されているのでIntent.URI_INTENT_SCHEMEではなくそちらを使った方がよいのだろうが、URI_ANDROID_APP_SCHEMEはAPIレベル22から入っているフラグで、今回のアプリはminSDKVersionがそれ未満だったのでIntent.URI_INTENT_SCHEMEでパースしてしまっている。
ちなみに
このリンクの仕様にのっとって自力でパースしてIntentをつくることすることも可能とは思いますがものすごく手間だしパースできる機構が用意されているのだからそちらを使う方がいいと思います。(自分は最初この機構に気づいておらず自力でパースするコードを作ってしまっていました。半日以上かかりました;)
追記 : 自分でハンドリングするようにするとこんな感じ
まず、どこかに以下のようなメソッドを定義してやって
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://~形式も規格に沿ってパースしてやればうまくハンドリングできるはず。)
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アプリを参考に
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
}
と加工してやる。