2011年12月3日 星期六

Android 問題百出之 2.3.x 的 JavaScript Interface

Standard
有鑑於 Android 問題太多,只好定了個系列標題『Android 問題百出』當開頭,並將碰到的問題和解決或避開的方法記錄在內。話說回來,筆者個人其實相當討厭 Android,自 Android 出現以來,從未真的投入其中並賺過什麼錢,會接觸,多半是興趣玩弄或是幫一些朋友的公司臨時打工救火。不過,既然是救火,任何千奇百怪的問題或狀況都會遭遇到,甚至還得『限時』解決別人解決不了的問題。

這次碰到的問題就是 Android 2.3.x Gingerbread 缺少 JavaScript Interface 的實作,如果你的應用程式有自己實作 API 供 JavaScript 程式使用,那這些 API 將會完全失效。而這樣的問題,對於使用 Web 技術(HTML5 + Javascript)的應用程式來說,相當嚴重。

話說 Android 在 2.3 版本之後,採用了 V8 做為 JavaScript Engine。在快速掃過 Android 關於 WebView 和 WebKit 的程式碼後,發現不幸的是『Google 再次未將程式寫完』。但這一次,不只是功能未完成,而是將原本可以用的功能(在 Android 2.2),變成不能用。這讓筆者非常不能理解,為何 Google 換了 V8 Engine 後,明知功能沒有改完,卻仍將這部份實作隨新版 Android 釋出?然後,絕大多數廠商,在毫無感覺之下,直接繼承了這樣的 Bug。

對硬體裝置的廠商來說,重新實作這個 JavaScript Interface 支援是最佳的解決辦法,髒一點的方法是將 V8 改回舊的 JSCore。但很可惜的是,就算有人改好了,也沒有廠商願意釋出這部份的程式碼。(所以才會不停有人願意花大錢找筆者這種臨時救火工呀。)

此外,如果你是一般的應用程式開發者,因為動不了底層,則完全無解。但是,這邊有個 Workaround,可以暫時代替 JavaScript Interface,讓應用程式開發者可以避開這問題(以下假設自定的 JavaScript Interface Class 為 myAPI)。

定義將會用到的變數:
/* Define private variable */
private static WebView mWebView;
private static myAPI mJSIF;
private static boolean javascriptInterfaceBroken = false;

初始化 WebView 時加上 Workaround:
/* Initializing WebView */
mWebView = (WebView) findViewById(R.id.supermark_dialog);
mWebView.getSettings().setJavaScriptEnabled(true);

/* Check Android version */
try {
    if (Build.VERSION.RELEASE.contains("2.3.")) {
        javascriptInterfaceBroken = true;
    }
} catch (Exception e) { }

/* Create own JavaScript Interface */
mJSIF = new myAPI(mWebView);

if (!javascriptInterfaceBroken) {
    mWebView.addJavascriptInterface(mJSIF, "Fred");
} else {
    /* Workaround to add JavaScript Interface to webview for Fucking Android 2.3 */
    mWebView.setWebViewClient(new WebViewClient() {
        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            if (javascriptInterfaceBroken) {
                String handleGingerbreadStupidity =
                    "javascript: function do() {window.location='http://Fred:do:null';};" +
                    "javascript: function handler() {" +
                    "this.do=do;" +
                    "}; " +
                    "javascript: var Fred = new handler();";
                view.loadUrl(handleGingerbreadStupidity);
            }
        }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            if (javascriptInterfaceBroken) {
                if (url.contains("Fred")) {
                    /* Parsing URL */
                    StringTokenizer st = new StringTokenizer(url, ":");
                    st.nextToken();
                    st.nextToken();
                    String function = st.nextToken();
                    String parameter = st.nextToken();

                    /* Call function */
                    if (function.equals("do")) {
                        mJSIF.do();
                    }
                }

                return true;
            }

            return false;
        }
    });
}

最後,我們還是可以如使用原生的 JavaScript Interface 一般,在 HTML5 + JavaScript 中呼叫自定的 API:
<script>
window.Fred.do();
</script>

簡單來說,就是創造假的 URL 和相應的 Parser,去接收從 JavaScript 傳來的要求。

後記

如果說一直專心並精通於某樣領域的人稱之為專家,那筆者肯定不是 Android 專家。但是,好在長久來與 Open Source 拼命所練就出來的功夫,無論什麼樣的火,總算都能夠快速找到火源和撲滅,應該可以算是滅火專家吧。 :-D