[メモ]Android WebViewでの処理

BLUE GIANT面白い!

BLUE GIANT 8 (ビッグ コミックス〔スペシャル〕)
小学館
2016-03-30
石塚 真一

amazon.co.jpで買う
Amazonアソシエイト by BLUE GIANT 8 (ビッグ コミックス〔スペシャル〕) の詳しい情報を見る / ウェブリブログ商品ポータル




いや、前回、2月頃からぼちぼち落ち着きそう、などと書いておいて、いきなり2月後半にならないとブログの更新が出来ないなんて言う状況で、なおかつ仕事のメモ的な内容のみというのが申し訳ないので、関係ない話題から入ってみただけです。すみません。


今担当している仕事でAndroidアプリを作ってます。
AndroidのWebViewを使って、サイトを表示して処理するだけのアプリだったんですが、これがまぁ、予想外に大変でして…。
調べてはじめて知ったんですが、AndroidのWebViewで色んなことやらせようとしたら、かなりの鬼門だったんですね。
ファイル選択、ファイルアップロード、その他諸々、Android5.0以降なら結構簡単に、まともに動くんですが、4以前のバージョンにも対応しようと思うと、かなり細かいチューニングが必要でした。
おまけに、4.1系と4.2系と4.4系でそれぞれ動きが違うとか、もうなんなんだよ!って感じで…

一番苦労したのが、WebView内で何かのリンクをタップしたときのイベントをキャプチャーして別の処理を実行させるときなんですが、そのイベントを発火させた上で他のイベントを実行したいときと、イベントは発火させないで他の処理をさせたいときが混在するような状況で、それぞれ使い分けをするというのがかなり苦労しました。

今後も同じような話があるかもしれないということで、その内容をメモしておきます。
以下、赤字の()内はブログに書くためのコメント。


public class WebViewActivity extends Activity {

(定数や変数の定義は中略)

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web_view);


 (これを書いておかないと、ファイルのダウンロードが動かない)
/** ファイルダウンロード処理 **/
webView.setDownloadListener(new DownloadListener() {

@Override
public void onDownloadStart(String url, String userAgent,
String contentDisposition, String mimetype,
long contentLength) {

String[] values = contentDisposition.split("filename=");
String filename = values[1].replaceAll("\"", "");

DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));

String cookies = CookieManager.getInstance().getCookie(getString(R.string.host_browse_protocol) + getString(R.string.host_browse_domain));
request.allowScanningByMediaScanner();
request.addRequestHeader("Cookie",cookies);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setVisibleInDownloadsUi(true);
request.setTitle(filename);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
DownloadManager dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
dm.enqueue(request);
}
});

webView.setWebViewClient(new WebViewClient() {
(通常、WebView内のリンクをクリックされたときのイベントはshouldOverrideUrlLoadingを使ってキャプチャーする。ただし、4.x系のAndroidでは、このイベントは、ブラウザのURLが変更されるときしか発火しない。つまり、例えばAjaxを使ったリンクをクリックしたようなイベントでは発火されない。このため、次に出てくるonLoadResource内で関数を意図的にコールしてイベントを発火させた)
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//true をリターンすると、自身のWEBView上は、ページ遷移の処理をしなくなる
(何らかの処理対象にしたいイベントかどうかは、引数で渡ってくるurlで判定する)
if (url.startsWith(WebView.SCHEME_TEL)) {
(処理中略)
return true;
} else if (url.startsWith(getString(R.string.browser_url_portal)) ||
url.startsWith(getString(R.string.browser_url_apply)) ||
url.startsWith(getString(R.string.host_browse_protocol) + getString(R.string.host_browse_domain) + getString(R.string.browser_url_other))
) {
(処理中略)
return true;
} else if (url.endsWith(getString(R.string.browser_str_handwriting))) {
(処理中略)
return true;
}
(falseを返せば、普通にリンクイベントが継続される)
return false;
}

(onLoadResourceは、WebView内で、特定のリソースを読み込む時に発火するイベント。ページ内の画像ファイルやリンク方式になっているjs/cssファイルなど、リソースを読み込むたびに発火されるので、通常1ページを読み込んだ時には数回発火される。Android4.x系ではAjaxのイベントが拾えなかったため、この中でイベントを捕まえて処理する必要がある。ただし、4.x系では、この中でイベントを捕まえること自体は出来るが、読み込み自体をキャンセルする(元々のイベントを発火させない)ことは出来なかった)
@Override
public void onLoadResource(WebView view, String url) {

if (url.endsWith(getString(R.string.browser_str_handwriting)) && !org_url.endsWith(getString(R.string.browser_str_handwriting))) {
org_url = "";
/** 4.x系でshouldOverrideUrlLoadingが呼ばれないための対策。これでページ遷移しないように出来る? **/
this.shouldOverrideUrlLoading(webView, url);
} else if ((url.startsWith(getString(R.string.host_browse_protocol) + getString(R.string.host_browse_domain) + getString(R.string.browser_url_fax_download))
|| url.startsWith(getString(R.string.host_browse_protocol) + getString(R.string.host_browse_domain) + getString(R.string.browser_url_msg_download)))
&& !url.toString().equals(org_url)
) {
org_url = "";
/** Android5では不要。バージョンによってきりわけが必要か。 **/
}
org_url = url;
}

(4.x系で、onLoadResourceの中でイベントをキャンセルすることが出来なかったため、レスポンスをWebView画面に表示するshouldInterceptRequest関数の中で、元々開いていたページのHTMLの内容を変数内に取得しておき、その内容がレスポンスとして返ってきたような振る舞いをさせることで、あたかも、イベントそのものが発火していないかのように見せる対応を実施)
/** API21より前ではJavascriptでの読み込みにshouldOverrideUrlLoadingが反応しないため、こちらで実現する 5.0以降では不要なため、ひとまずコメントに
* 手書きへのページ遷移はonLoadResourceで、ページ遷移をしない処理をこっちで行う **/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH | Build.VERSION_CODES.JELLY_BEAN | Build.VERSION_CODES.JELLY_BEAN_MR1 | Build.VERSION_CODES.JELLY_BEAN_MR2 | Build.VERSION_CODES.KITKAT)
@Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (url.endsWith(getString(R.string.browser_str_handwriting)) && !org_url.endsWith(getString(R.string.browser_str_handwriting))) {
previewHtml=loadHtml.htmlContents;
InputStream bais = new ByteArrayInputStream(previewHtml.getBytes());
this.shouldOverrideUrlLoading(view, url);
return new WebResourceResponse("text/html","utf-8",bais);
}
return null;
}

(shouldInterceptRequest内でページ内のHTMLを置き換えるために、ページの読み込みが完了するたびに、その内容を変数内に保存するJavascriptの処理を実行した)
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
view.loadUrl("javascript:window.HTMLOUT.processHTML(''+document.getElementsByTagName('iframe')[0].contentDocument.getElementsByTagName('html')[0].innerHTML+'');");
}

});

webView.setWebChromeClient(new WebChromeClient() {


(Android4.x系でのファイル選択のための処理。ただし、4.4ではこのやり方が動かなくなっている。どうやらopenFileChooserは非公開の内部API関数らしいのだが、4.4系ではこのやり方が使えなくなっている。更に公式の対応方法も5以降にならなければ公開されておらず、実質4.4ではファイル選択は実行できない模様。Javascriptでゴリゴリかけば出来ないわけではない、とのことだが、今回は期限との兼ね合いなどもあり対応は見送り)
/**** ファイル選択のための処理を追加 ****/
public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) {
mUploadMessage = uploadMsg;
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*");
startActivityForResult(Intent.createChooser(i, "title"), FILECHOOSER_RESULTCODE);
}

(Android5系ではこれでOK)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) {
//return super.onShowFileChooser(webView, filePathCallback, fileChooserParams);
if( lollipopUpMsg != null) {
lollipopUpMsg.onReceiveValue(null);
lollipopUpMsg = null;
}
lollipopUpMsg = filePathCallback;
Intent intent = fileChooserParams.createIntent();
try {
WebViewActivity.this.startActivityForResult(intent,
FILECHOOSER_RESULTCODE);
} catch (ActivityNotFoundException e) {
lollipopUpMsg = null;
return false;
}
return true;

}
});

webView.loadUrl(webUrl);

}


/** File選択ダイアログから戻ったときの後処理 **/
@Override
protected void onActivityResult(int requestCode, int resultCode,Intent intent) {
if (requestCode == FILECHOOSER_RESULTCODE && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP ) {
if ( mUploadMessage == null ){
return;
}
Uri result = (intent == null || resultCode != RESULT_OK) ? null : intent.getData();
mUploadMessage.onReceiveValue(result);
mUploadMessage = null;
}else if(requestCode == FILECHOOSER_RESULTCODE ) {
if ( lollipopUpMsg == null ){
lollipopUpMsg.onReceiveValue(null);
return;
}
receiveValue5(resultCode,intent);
lollipopUpMsg = null;
}
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void receiveValue5(int resultCode,Intent intent){
lollipopUpMsg.onReceiveValue( WebChromeClient.FileChooserParams.parseResult(resultCode, intent) ) ;
}


}



てなわけで、AndroidのWebViewを活用したアプリを作る場合は、対象バージョンによってはかなりの労力を要することになり、色んな落とし穴にはまる可能性がありますからご注意を!
5.0以降のみの対応と言うことで良ければ、Javascriptも大概そのまま動きますし、比較的容易に対応できるかと思います。


では、今度こそあまり長く間を置かないうちに更新が出来るように頑張ります!


あ、そうそう。ついでにもう一つご報告を。
4月ごろから転勤で名古屋に行くことになりました。
転々と引っ越してはいたものの約10年間過ごした九州を離れることになります。


ではまた~~~。

この記事へのコメント

この記事へのトラックバック

テーマ別記事