Android アプリの WebView 連携
この前 iOS アプリの UIWebView 連携という記事を書いたが、その対となるものとして Android アプリにおける WebView
連携についてまとめてみる。
はじめに
Android SDK は WebView
という Web ブラウザ コントロールを提供している。これは iOS における UIWebView
に相当し、ページ表示とアプリ連携をサポートしている。本記事では iOS の UIWebView
連携で実装したサンプルを Android に移植しながら WebView
連携の実装手順や考察をまとめる。
ローカル HTML と assets
Android アプリ内にローカル HTML を組み込む場合、assets を利用することになる。Eclipse + ADT を利用した Android プロジェクトの場合、その直下に assets というディレクトリが用意されている。ここに配置したものはフォルダ階層を維持した状態でアプリから参照できる。
assets
内の HTML を WebView へ表示する場合は WebView.loadUrl
に file:///android_asset/
から始まるパスを指定すればよい。assets
直下の html
という名前のフォルダ内にある basic.html
というファイルが対象なら以下のようになる。
// WebView mWebView が定義されているものとする
this.mWebView.loadUrl( "file:///android_asset/html/basic.html" );
assets
にはどのようなファイルも配置可能だが、未圧縮ファイルの場合は 1 〜 2MB ほどの容量制限 (上限は端末依存) がある。ここは小さなファイルを置く場所なので大きくなりそうなら res/raw
を利用したほうがよいだろう。
ローカル HTML の構成物に大きな画像や音声ファイル、Web フォントなどが含まれる場合はリンク指定を Web にして動的にダウンロードされるようにするなどして容量制限を回避する必要あり。ローカル HTML 全体を ZIP 圧縮したものを組み込んで動的に展開してもよいだろう。
assets
は配置されたファイルをそのまま組み込むため意図せぬファイルが含まれぬよう注意する。例えば Mac の .DS_Store
や Windows の thumb.db
、Git リポジトリの設定ファイル (ローカル HTML だけ Git submodule で組み込んでいるとやりがち) など。これらが組み込まれるとアプリのサイズ増大やセキュリティ的な問題 (開発環境の情報などが漏れる) を引き起こす可能性がある。
組み込み対象をフィルタリングできればよいのだが Eclipse + ADT にはそういう機能が用意されていないらしく自衛するしかなさそうだ。
アプリから WebView 上の JavaScript 関数を実行する
アプリから WebView
の JavaScript 関数を実行してみる。まず HTML に以下の <div>
を定義。アプリから指定されたテキストを受け取りここへ表示したい。
<div class="from-app-text"></div>
次に HTML から読み込まれた JavaScript 上で以下の関数を定義。関数がグローバルに定義されていることが重要で名前については重複しなければ何でもよい。たとえばプロジェクト名を接頭語として付与してもよいだろう。この関数を実行すると引数に指定されたテキストが前述の <div>
へ表示されるようにしておく。
window.webViewCallback = function( text ) {
$( ".from-app-text" ).text( text );
};
アプリ側からこの関数を実行する処理は以下。
/**
* WebView 上に読み込まれた JavaScript の関数を実行します。
*
* @param param JavaScript の関数へ指定するパラメータ。
*/
private void executeJavaScriptFunction( String param ) {
final String script = "javascript:window.webViewCallback('%s');";
this.mWebView.loadUrl( String.format( script, param ) );
}
Android の WebView
には iOS の UIWebView - stringByEvaluatingJavaScriptFromString
相当のメソッドはない。かわりに javascript: から始まる JavaScript を記述してそれを WebView.loadUrl
へ指定することでスクリプトが実行される。
WebView
上で JavaScript を有効にする場合は WebView.getSettings
で取得した WebViewSettings
に対し setJavaScriptEnabled
を呼び出す必要があるのだけど、そうすると以下のコンパイラ警告がでる。
Using setJavaScriptEnabled can introduce XSS vulnerabilities into you application, review carefully.
これは JavaScript を有効にすることでアプリに XSS の脆弱性を含んでしまう可能性がありますよ、というもの。リスクを承知したうえで警告を抑止する場合は WebViewSettings.setJavaScriptEnabled
を呼び出すメソッドか、それを含むクラスに以下のアノテーションを追加する。
@SuppressLint( "SetJavaScriptEnabled" )
たとえローカル HTML であっても Web 側のリソースを参照する場合は危険と隣合わせであることは意識しておきたい。後述する Google Maps API 連携のように Web サービスを参照するならサイトが乗っ取られて API の中身が入れ替わってアプリ側へのコールバックが悪用される危険性もある。そのためアプリ側のインターフェースは非公開としておくとかコールバックのパラメータ妥当性を検証するといった対策をしておくのがよいだろう。
WebView 上の JavaScript からアプリへコールバックする
WebView
上の JavaScript からアプリ側へコールバックする処理を実装してみる。二通りの方法があるので両方紹介しておくが、サンプルでは location.href
の方だけ採用している。
JavaScriptInterface
WebView
上からアプリ側へコールバックする一般的な方法は JavascriptInterface になるだろう。これはアプリ側で実装したクラスを WebView
上の JavaScript Object として公開する機能である。例えば以下のようなクラスを定義する。
/**
* WebView 上の JavaScript に公開するインターフェースです。
*/
class MyJavaScriptInterface {
/**
* アラートを表示します。
*
* @param message メッセージ。
*/
@JavascriptInterface
public void showAlert( String message ) {
// アラート表示処理 ...
}
}
このクラスのインスタンスを生成して WebView.addJavascriptInterface
の第一引数へ指定することで、第二引数の名称をもったオブジェクトとして WebView
上の JavaScript へ公開される。
MyJavaScriptInterface obj = new MyJavaScriptInterface();
this.mWebView.addJavascriptInterface( obj, "appJsInterface" );
JavaScript 側からの操作は以下。
appJsInterface.showAlert( "message" );
パラメータの型は自動的にマーシャリングされる。String
や Boolean
はそのまま、Number
なら Integer
、Double
、Float
などへ対応づけられる。
JavaScriptInterface
利用上の注意点。これを実装したクラスだけで処理が完結することは考えにくい。そのため Activity
を Context
として参照させたり Listener 経由でコールバック機構を設けたりすることになるだろう。型の自動解決や JasScript のオブジェクトとして操作可能なことから非常に便利な機能ではあるけれど想定外のメソッド呼び出しやフィールド参照が実行される脆弱性がある。
- AndroidのWebView#addJavascriptInterfaceがどれだけ危険か検証してみた
- AndroidのWebView#addJavascriptInterfaceは基本使わない方がいい、っていう話
Android 4.2 以降ではこの問題への対策として JavascriptInterface
アノテーションが指定された public
メソッドのみを許可するための制限が追加された。とはいえ Android 4.2 未満では利用できないし WebView
連携するページを iOS と共通にする際にコールバック部分を Android バージョンで分岐するのもイマイチ。というわけで次項では JavaScriptInterface
以外の方法で連携する方法を紹介する。
location.href によるコールバック
iOS アプリの UIWebView 連携で紹介した方法を Android に移植してみる。JavaScript 側の処理は iOS と共通である。
location.href = "app-callback://test";
location.href
にリクエストを指定するとその内容にしたがって Web ブラウザーはページ遷移する。今回はコールバックが目的なのでリクエストとなる URL のスキーム部分に独自の文字列を指定する。スキームの名称は RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax に定義された文字の範囲であればなんでもよい。今回は app-callback
としているが、アプリのプロジェクト名を含めてユニークなものとするのが好ましい。
アプリ側でページ遷移をハンドリングするには WebViewClient.shouldOverrideUrlLoading
を実装して対象となる WebView
へ指定する。
/**
* 指定された URL が WebView からのコールバックであることを調べ、対応する処理を実行します。
*
* @param url URL。
*
* @return コールバックだった場合は true。
*/
private boolean checkCallbackUrl( String url ) {
final String CallbacScheme = "app-callback://";
if( !url.startsWith( CallbacScheme ) ) { return false; }
String message = url.substring( CallbacScheme.length() );
new AlertDialog.Builder( this )
.setTitle( R.string.text_from_webview )
.setMessage( message )
.setPositiveButton( "OK", null )
.show();
return true;
}
/**
* WebView を初期化します。
*/
@SuppressLint( "SetJavaScriptEnabled" )
private void initWebView() {
this.mWebView = ( WebView )this.findViewById( R.id.webViewBasicCoordination );
this.mWebView.getSettings().setJavaScriptEnabled( true );
this.mWebView.setWebViewClient( new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading( WebView view, String url ) {
return checkCallbackUrl( url );
}
} );
this.mWebView.loadUrl( "file:///android_asset/html/basic.html" );
}
WebViewClient.shouldOverrideUrlLoading
には遷移先となる URL 文字列が指定されるため、コールバック用スキームであることを判定する。コールバック用なら独自処理。shouldOverrideUrlLoading
の戻り値は独自処理を実行してページ遷移をキャンセルするなら true
、WebView 既定の処理にまかせる場合は false
を返す。コールバック処理ならページ遷移は不要なので true
にしておく。
ここまでの処理を利用して簡単なサンプルを実装してみた。表示しているローカル HTML は iOS のサンプルへ組み込んでいたものから一切、手を加えていない。
画面上部の EditText
に文字を入力してキーボードを Done で閉じると JavaScript が実行され、HTML 側のテキストが更新される。HTML 上の青い領域をタップするとアプリ側へのコールバックが実行され JavaScript 側で定義されたテキストを AlertDialog
で表示。
Google Maps API と連携してみる
iOS 版のサンプルで Google Maps API 連携してみたが、ローカル HTML 部分はそのままにアプリ側の処理を Android に移植してみる。まず JavaScript への住所指定については iOS とほぼ変わらない。単に WebView.loadUrl
でスクリプト実行するだけである。
/**
* マップの中央位置を指定された住所の位置へ移動させます。
*
* @param address 住所。ジオコーディングによって検索されます。
*/
private void moveToMapCenter( String address ) {
final String region = this.getString( R.string.google_map_rgion );
final String script = "javascript:window.webViewCallbackSearchAddress('%s','%s');";
this.mWebView.loadUrl( String.format( script, address, region ) );
}
次に WebView
上の JavaScript からのコールバックだが、これも大まかな処理の流れは一緒。ページ遷移をアプリ側で検出して URL がコールバックであることを判定した後にパラメータを解析する。以下の checkCallbackUrl
は WebViewClient.shouldOverrideUrlLoading
から呼び出される。
/**
* 指定された URL が WebView からのコールバックであることを調べ、対応する処理を実行します。
*
* @param url URL。
*
* @return コールバックだった場合は true。
*/
private boolean checkCallbackUrl( String url ) {
final String CallbacScheme = "app-callback://map";
if( !url.startsWith( CallbacScheme ) ) { return false; }
Map< String, String > params = this.parseUrlParameters( url );
String address = params.get( "address" );
String latitude = params.get( "lat" );
String longitude = params.get( "lng" );
Log.d( "HostWebView", String.format( "address = %s, latitude = %s, longitude = %s", address, latitude, longitude ) );
if( address != null ) {
this.mAddressTextView.setText( address );
}
return true;
}
/**
* URL におけるパラメータ部分を解析してディクショナリ化します。
*
* @param url URL。
*
* @return 解析結果。
*/
private Map< String, String > parseUrlParameters( String url ) {
Map< String, String > result = new HashMap< String, String >();
int index = url.indexOf( "?" );
if( index == -1 ) { return result; }
String[] params = url.substring( index + 1 ).split( "&" );
for( String param : params ) {
String[] keyValuePair = param.split( "=" );
if( keyValuePair.length >= 2 ) {
try {
String value = URLDecoder.decode( keyValuePair[ 1 ], "utf-8" );
result.put( keyValuePair[ 0 ], value );
} catch( UnsupportedEncodingException e ) {
e.printStackTrace();
}
}
}
return result;
}
Android 上でもバッチリ動作している。このスクリーンショットではハチ公で検索して渋谷駅近辺が表示されている。ところでハチ公は Google Maps API 的にも渋谷なのだろうか?
サンプル プログラム
今回の内容を実装したサンプル プログラムのプロジェクト一式を GitHub にて公開した。開発は Eclipse Kepler Service Release 1 + ADT 22.3、動作確認には Xperia NX (SO-02D) を使用。ライセンスは The MIT License (MIT)。fork、改変などはご自由にどうぞ