Android 2.3 @JavascriptInterface Issue
JavascriptInterface 這個問題相信 Google 一下就可以找到很多文章,雖然如此小蛙還是試了好久才成功,這篇文章記錄一下當時小蛙不成功的盲點在哪裡。上一篇文章「Android 與 WebView 中的 Javascript 相互溝通」記錄怎麼讓 Android 與 Javascript 相互溝通。
小蛙在實作 WebView 載入的網頁與 Android 互動時,一直都以手邊的 Nexus 7 2013 來做測試,直到所有功能都已就緒,有一天突然想起之前好像有看到 Javascript 在 2.3.x 會出現錯誤的問題,於是開了 2.3 版本的 emulator 來做測試,由於小蛙實作的部分是從 Javascript 呼叫某一個 function 來實作開啟行事曆的功能,這邊發生了大概類似下面的這種錯誤(小蛙的錯誤訊息已經洗掉了,以下內容來字參考資料1)
JNI WARNING: jarray 0xb5d256f0 points to non-array object (Ljava/lang/String;) "WebViewCoreThread" prio=5 tid=8 NATIVE | group="main" sCount=0 dsCount=0 obj=0xb5cfc348 self=0x8234e98 | sysTid=2023 nice=0 sched=0/0 cgrp=[fopen-error:2] handle=136531904 at android.webkit.WebViewCore.nativeTouchUp(Native Method) at android.webkit.WebViewCore.nativeTouchUp(Native Method) at android.webkit.WebViewCore.access$3300(WebViewCore.java:53) at android.webkit.WebViewCore$EventHub$1.handleMessage(WebViewCore.java:1162) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:130) at android.webkit.WebViewCore$WebCoreThread.run(WebViewCore.java:633) at java.lang.Thread.run(Thread.java:1019) VM aborting
對於這個問題,參考資料1 的作者將 JavascriptInterface 整理成一個比較完整的 solution,不過因為教學文章「漏漏長」,所以小蛙後來沒有採用這個方案,雖然方案不同,不過解決這個問題的邏輯是差不多的。
小蛙後來使用了參考資料2的方法來解決,這邊大概記錄一下作法 (不知道怎麼讓 Android 與 Javascript 互通的話可參考「Android 與 WebView 中的 Javascript 相互溝通 @ 蛙齋」):
檢查 Android 版本
設定參數來記錄 Android 版本是否為 2.3.x
private static JavaScriptInterface JSInterface; // 設置 javascriptInterfaceBroken 來判斷是否為 2.3.x private static boolean javascriptInterfaceBroken = false; public static WebView wv; try { if (Build.VERSION.RELEASE.contains("2.3.")) { javascriptInterfaceBroken = true; } } catch (Exception e) { } JSInterface = new JavaScriptInterface(getActivity());
依照版本設定對應動作
if(!javascriptInterfaceBroken){ // 非 2.3.x 版本,正常執行 wv.addJavascriptInterface(JSIterface, "JSInterface"); ... }else{ // 2.3.x 版本 wv.setWebViewClient(new WebViewClient(){ @Override public void onPageFinished(WebView view, String url){ super.onPageFinished(view, url); String handleGingerbreadStupidity = "javascript:function addToNotify(title, url, start, end) { window.location='http://JSInterface:addToNotify:'+(title)+'◎'+(url)+'◎'+(start)+'◎'+(end); }; " + "javascript: function handler() { this.addToNotify=addToNotify }; " + "javascript: var JSInterface = new handler();"; view.loadUrl(handleGingerbreadStupidity); } @Override public boolean shouldOverrideUrlLoading(final WebView view, final String url) { if (url.contains("JSInterface:addToNotify")) { // 在這邊可用 regular expression 把剛剛組出來的 url 解回 // 須注意如果有中文的話,字串要先從 ISO-8859-1 轉回 UTF-8 String nUrl = new String(url.getBytes("ISO-8859-1"), "UTF-8"); String title = "title"; String desc = "desc"; String start = "start"; String end = "end"; try { Method sMethod = JSInterface.getClass().getMethod("addToNotify", new Class[] { String.class, String.class, String.class, String.class }); sMethod.invoke(JSInterface, title, desc, start, end); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } return true; } }); }
上述程式碼中有幾個部份小蛙提出來記錄:
handleGingerbreadStupidity 字串中的 addToNotify 改成自己的方法名稱;JSInterface 改成自己的 JavaScriptInterface 名稱;後面參數有幾個都用「:」連接,因為小蛙的參數中有冒號會跟原本的冒號混淆,導致要拆出字串的時候很麻煩,因此這邊改成用「◎」來區隔。
shouldOverrideUrlLoading 會接收到從 onPageFinished 過來的 url,接著判斷如果包含剛剛我們自己組合的字串的話,就使用 reflection 的方式來呼叫對應方法。JSInterface.getClass().getMethod 中的 JSInterface 是開頭宣告的 JavaScriptInterface 的物件名稱,getMethod 第一個參數為呼叫的方法名稱,第二個參數是傳入的參數型別,都設定好之後就用 sMethod.invoke 呼叫該方法並傳入需要的物件及參數。
記錄的有點亂,小蛙當時卡在 onPagefinished 中傳參數的部分,因為內容包含了冒號導致發生一些問題;再來是 getMethod 方法不太會用,卡了一段時間 …