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