Android WebViewにzipアーカイブしたHTMLをロードするZipArchivePathHandlerを作った
小ネタ。
事前知識: WebViewAssetLoader
Android 5.0以降のWebView
には、URLに対するリクエストのレスポンスをカスタマイズできる機能がある。webViewClient
プロパティ(JavaならsetWebViewClient()
の引数)で指定したWebViewClient
のインスタンスでshouldInterceptRequest()
が返したWebResourceResponse
がnullでなければ、その内容がレスポンスとして扱われる。WebResourceResponse
はHTTPステータスコードから指定して返せるので、このメソッドをオーバーライドした独自のWebViewClient
を渡してやれば、いわゆるURLフィルタリングが可能になる。
この仕組みに基づいて、androidx.webkit
にはWebViewAssetLoader
という独自のWebリソース インターセプターが提供されている。これは、"https://appassets.androidplatform.net/" で始まるURLへのリクエストに対して、実際のWebのURLではなく、WebViewAssetLoader.PathHandler
というインターフェースの実装をもとにレスポンスを返すようになっていて、このインターフェースの応用としてWebViewAssetLoader.AssetsPathHandler
(Androidアプリのassets
の内容をもとにレスポンスを返す)、WebViewAssetLoader.ResourcesPathHandler
(res
の内容をもとにレスポンスを返す)といった仕組みが用意されている。
shouldInterceptRequest()
ではこんな感じでこのWebViewAssetLoader
を使う:
private val assetLoader = WebViewAssetLoader.Builder()
.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(ctx))
.build()
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
return assetLoader.shouldInterceptRequest(request.url)
}
whoisを見る限りandroidplatform.net
はGoogleがドメインを取得していて、このURLに実際に何かしらのWebコンテンツが存在するような事態は生じないと考えてよさそうだ。
実際にはこのPathHandlerは何を対象としても良いので(ただしWebViewAssetLoader
以下にあるインターフェースなので、"https://appassets.androidplatform.net/" のURLが前提だ)、assets
やres
みたいにアプリ内コンテンツだけでなく、独自にアプリ側で生成・取得した内容をWebViewに表示したい、という場合にも有効だ。今は流行っていないと思うけど、Webアプリケーションを.war
ファイルで配布していた時代があったことを考えると、Webサイト全体をアーカイブとして配布する仕組みが無かったわけでもない。
ZipアーカイブしたWebコンテンツをロードしたい
いま自分が作っているアプリというかフレームワークで、VSTやAUみたいなオーディオプラグインをホストする仕組みがあるのだけど、Androidでは一般的に複数のアプリのActivityを同時に立ち上げられないので、ホスト(DAWなど)のUIを出したままプラグインのUIを出すことができない。それではまともにプラグインを操作しながら打ち込み作業が行えないので、UIはプラグイン側からWebコンテンツとして提供する、という仕組みを構築している。イメージとしてはこんな感じだ:
WebViewではaddJavaScriptInterface()
メソッドを使って引数オブジェクト(型は何でもいい)にあるJavaScriptInterface
アノテーションの付いたメソッドをWebViewコンテンツ上のJavaScriptから呼び出せるようになっていて、この例だとプラグインパラメーターのツマミをいじるとそれがWebView側に飛ぶようになっている。
このWebコンテンツはg200kg/webaudio-controls、その前提となるwebcomponents-lite.js
、それからこのツマミの画像を使っていて、これをプラグイン側からホスト側に全部送ってもらう必要があるのだけど、面倒くさいのでzipで1回で済ませたいし、受け取った側はこれをどこかに一時的に展開したりするのは面倒だ。
どうせstaticなWebページなのだし、Zipアーカイブの中身を直接ロードできれば良いのではないか。そしてそのための仕組みは十分整っているはずだ。
ZipArchivePathHandler.kt
そういうわけで作った。50行も無い。CC0 (Public Domain)で公開しているので、自由にお使いいただきたい。
リクエストがある度にZipInputStreamを作って内容を見に行くのは効率が悪いと思うのだけど、java.util.zipのAPIではこんな実装になってしまう。zipにはエントリテーブルがあるはずだから、そこから直接ジャンプして展開できれば良いはずだ。
本当はjava.util...はやめたかったんだけど、pure Kotlinで実装しているライブラリはまだ無さそうだったし、まあAndroid用だからいいだろうと考えた。ちなみにkotlinlang slackで訊いてみたら、okio 3.0ではサポートされる予定だと教えられた(ソースを見たらまだjvm実装しか無かったから、予定という感じだ)。
Discussion