はじめに
こんにちは。カイザーです。
最近、アプリはWebViewだけで、機能はWEB側で実装されるという、いわゆる「ガワネイティブ」アプリを実装する機会がありました。
その時、「スマホのChromeではちゃんと動いているのに、WebViewだと動かない!!」ということが多々あったので、紹介していきます!
JSのAlert, Confirm, Promptが表示されない!
まず、JavaScriptがWebViewの標準で無効になっているので、有効にしてあげましょう。
1 | webView.settings.javaScriptEnabled = true |
そして、WebChromeClientに対応するメソッドを実装していきます。
1番簡単なのは、下記のように、それぞれ全てfalseを返してあげることです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { return false } override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { return false } override fun onJsPrompt( view: WebView?, url: String?, message: String?, defaultValue: String?, result: JsPromptResult? ): Boolean { return false } |
表示結果は下記のようになります。(Confirmの場合)
「file:// のページ」というタイトルは、自動的に付加されます。
これを変更したい場合には、下記のように自前で表示させる必要があります。
※ダイアログ表示にMaterialDialogsを使用しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { view ?: return true MaterialDialog(view.context).show { title(text = message) positiveButton { result?.confirm() } negativeButton { result?.cancel() } onCancel { result?.cancel() } } return true } |
読み込み中の進捗を表示する
重いページを読み込むときは、ユーザにその進捗率を見せた方が良いでしょう。
先ほどのChromeWebClientに「onProgressChanged」というメソッドが定義されているので、こちらを実装します。
1 2 3 4 5 | webView.webChromeClient = object: MyWebChromeClient() { override fun onProgressChanged(view: WebView?, newProgress: Int) { progressBar.progress = newProgress } } |
これはActivityのコードですが、progressBarというIDで、Progress Bar(Hrizontal)を設置しています。
なお、newProgress引数には、0〜100の値が入るため、ProgressBarのMaxは100にしておきましょう。
UserAgent
アプリアクセスをサーバサイドで判断する場合などにおいて、WebViewに独自のUserAgentを設定しなければならない場合もあると思います。
その場合は、下記のように設定します。
1 | webView.settings.userAgentString += " 設定したいUserAgent" |
この例では、既存のuserAgentStringに、文字列を連結しているため、既存のUserAgentと共に、設定したいUserAgentが連結された上で、送信されます。
なお、「+=」ではなく「=」とすれば、既存のUserAgentをそのまま上書きできます。
クリアテキスト設定
アプリ内で平文通信を行わない、もしくは行うとしても限定的、というアプリの場合、意図しない平文通信が行われないよう、対策をしておきましょう。
クリアテキスト設定自体は、Android 6から使用可能ですが、WebViewに適用されるのはAndroid 7からです。
また、Android 9では、デフォルトでクリアテキストが無効になっているため、HTTP通信できません。
そのため、どうしてもHTTP接続をしたい場合は、後述するドメイン単位での許可を行う必要があります。
クリアテキストを無効にする
AndroidManifest.xmlを開き、applicationの開始タグに下記を追加します。
1 | android:usesCleartextTraffic="false" |
これで、クリアテキストが無効となり、HTTP通信が不可能になります。(Android 9では、設定せずともデフォルトでfalseです)
HTTP許可ドメインを設定する
止むを得ず、一部ページがクリアテキストである場合は、ドメイン単位で許可することができます。
まず、res/xmlフォルダを作り、下記のようなXMLを作成します。
1 2 3 4 5 6 | <?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">kantei.go.jp</domain> </domain-config> </network-security-config> |
今回は、domainタグで「includeSubdomains」をtrueとしているため、kantei.go.jpのサブドメインも対象としています。
ファイル名は「network_security_config.xml」とします。
そして、AndroidManifestのapplication開始タグに下記を加えます。
1 | android:networkSecurityConfig="@xml/network_security_config" |
これで、WebViewでアクセスしてみると、「http://www.kantei.go.jp/」などが表示できます。
クリアテキストを許可する
非推奨ですが、HTTPを全体的に許可したい場合は、クリアテキスト自体を許可します。
Android 9未満ではデフォルトで許可されていますが、Android 9からデフォルトで不許可であるため、明示的に許可を宣言します。
「AndroidManifest.xml」を開き、applicationの開始タグに下記を設定します。
1 | android:usesCleartextTraffic="true" |
これで、クリアテキストが許可されます。
ファイルダウンロードの実装
JavaScriptの「Blob」を使用して、JavaScriptで生成されたファイルをダウンロードさせる場合、WebView内からデータを取り出す必要があるため、「DownloadListener」と埋め込みJavaScript発行による実装が必要になります。(実装にあたりこちらを参考しました。)
DownloadListenerの実装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | webView.setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> val js = "javascript:" + "var xhr = new XMLHttpRequest();" + "xhr.open('GET', '" + url +"', true);" + "xhr.responseType = 'blob';" + "xhr.onload = function(e) {" + " if (this.status == 200) {" + " var reader = new FileReader();" + " reader.readAsDataURL(this.response);" + " reader.onloadend = function() {" + " base64Str = reader.result;" + " AndroidNative.onDownloadBlob(base64Str)" + " }" + " }" + "};" + "xhr.send();" webView.loadUrl(js) } |
JavaScriptの文字列を生成し、「webView.loadUrl(js)」で実行させます。(ブックマークレットと同じ仕組みです。)
JavaScriptの内容はシンプルで、「XMLHttpRequest」を使用して、受け取ったURLを読み込ませます。
読み込みが完了したら、「AndroidNative.onDownloadBlob()」を呼び出すことで、コールバックします。この関数は、後ほど定義していきます。
JavascriptInterfaceの実装
JavaScriptからコールバックされる「AndroidNative.onDownloadBlob()」を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class BlobJavascriptInterface { @JavascriptInterface fun onDownloadBlob(xhrResult: String) { // XHRのResult文字列から不要な箇所を取り除き、素のBase64文字列のみにする。 val base64 = xhrResult.replaceFirst("^data:([A-Z]|[a-z]|[0-9])+\\/([A-Z]|[a-z]|[0-9])+;base64,".toRegex(), "") // Base64デコード val blobBytes = Base64.decode(base64, 0) // デコードされた内容をファイルに書き込む val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "filename") val fileOutputStream = FileOutputStream(file, false) fileOutputStream.write(blobBytes) fileOutputStream.flush() } } |
メソッドのインタフェースが、そのままJSから呼び出せる関数になります。
XHRのResultはBase64ですが、先頭にBase64ではないデータが入っているため、それを正規表現で取り除いています。
あとは、素のBase64をデコードし、ファイルに書き込むだけです。
なお、この場合下記のパーミッションをAndroidManifestに記載し、ランタイムパーミッションの実装も必要になります。
1 | <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> |
ランタイムパーミッションの実装についてはここでは省略します。
実装にはこちらにページが参考になります。
JavascriptInterfaceの登録
JavascriptInterfaceを実装後、WebViewにあらかじめ登録しておく必要があります。
1 | webView.addJavascriptInterface(BlobJavascriptInterface(), "AndroidNative") |
第1引数には作成したJavascriptInterfaceのインスタンスを、第2引数にはインタフェース名を設定します。
(“AndroidNative.onDownloadBlob()”の、「AndroidNative」の部分になります。)
これで完了です。
さいごに
WebViewアプリを作ると言っても、Chromeアプリとはかなり異なる挙動をする場合が多くあります。
こういった事例は他もまだあるので、続編にて紹介したいと思います!