(Apologies in advance to my normal readers for this technical topic.)
The Google Android team released the Android 2.3 (“Gingerbread”) SDK two days ago, to much fanfare. This has sent the tech blogging world into a publishing frenzy, as it usually does. However, a potentially disastrous bug has surfaced that could crash literally thousands of apps in the Android Market immediately after opening the app.
The problem is described succintly here:
http://code.google.com/p/android/issues/detail?id=12987
In short:
Many apps show all or part of their UI with embedded WebViews that can render HTML.
Those WebViews make use of a great feature that bridges JavaScript (in the HTML) to the native Java code that “surrounds” the WebView.
This bridge is completely broken in Android 2.3. Trying to make even a basic call breaks the WebView immediately and crashes the app.
I believe members of the Android team are aware of the problem, and from early reports, it does not affect the Nexus S (the first Android 2.3 phone). This doesn’t really help those of us working against the emulator, however.
Here is a simple solution to work around this.
1.) In onCreate, check to see if the bridge is broken, and add the JavaScript interface only if not broken.
// Determine if JavaScript interface is broken.
// For now, until we have further clarification from the Android team,
// use version number.
try {
if ("2.3".equals(Build.VERSION.RELEASE)) {
javascriptInterfaceBroken = true;
}
} catch (Exception e) {
// Ignore, and assume user javascript interface is working correctly.
}
// Add javascript interface only if it's not broken
if (!javascriptInterfaceBroken) {
webView.addJavascriptInterface(this, "jshandler");
}
2.) Create a WebViewClient that passes in a new JavaScript object with the same name as your JavaScript interface object.
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
finishLoading();
// If running on 2.3, send javascript to the WebView to handle the function(s)
// we used to use in the Javascript-to-Java bridge.
if (javascriptInterfaceBroken) {
String handleGingerbreadStupidity=
"javascript:function openQuestion(id) { window.location='http://jshandler:openQuestion:'+id; }; "
+ "javascript: function handler() { this.openQuestion=openQuestion; }; "
+ "javascript: var jshandler = new handler();";
view.loadUrl(handleGingerbreadStupidity);
}
}
NOTE: for each of the Javascript-to-Java functions that you use, you will need to add a new Javascript function and pass it in as part of the loadUrl, like the defined below.
"javascript:function openQuestion(id) { window.location='http://jshandler:openQuestion:'+id; }; "
3.) In the same WebViewClient, override the URL handling portion to handle the URLs defined in step 2. After catching the URL, parse out the function and parameters, then use reflection to actually call the method.
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (javascriptInterfaceBroken) {
if (url.contains("jshandler")) {
// We override URL handling to parse out the function and its parameter.
// TODO: this code can only handle a single parameter. Need to generalize it for multiple.
// Parse out the function and its parameter from the URL.
StringTokenizer st = new StringTokenizer(url, ":");
st.nextToken(); // remove the 'http:' portion
st.nextToken(); // remove the '//jshandler' portion
String function = st.nextToken();
String parameter = st.nextToken();
// Now, invoke the local function with reflection
try {
if (sMethod == null) {
sMethod = MyActivity.class.getMethod(function, new Class[] { String.class });
}
sMethod.invoke(MyActivity.this, parameter);
} 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;
}
return false;
}
With this solution, you only need to make a few Android code changes and do not need to modify server output.
Comments welcome.
This is only a partial workaround.
Major issues:
This does not allow for a synchronous response from java. Any javascript code can be rewritten with return callbacks from java in a continuation passing style, but this generally requires a complete restructuring of the affected code base.
The javascript to java interface presented here does not persist across page loads. The original (broken) mechanism is available to any javascript executing within the webview, whether in the parent document or in any nested iframe, and is unaffected by content reloading.
Agreed on both points. That’s why this is a workaround, and only for simple cases; the real solution is for the Android team to actually fix the root cause.
It might be easier to use the WebChromeClient’s onJSAlert method instead. You could write a simple parser for all alerts that come by. You still need to use a continuation style, but it means you don’t have to reload the page…
This comment has been removed by the author.
I am beginner android programmer and most unlucky I have to demo on 2.3.
I cant understand two things that,
finishloading() function and sMethod() are not available in webview client to use. Can you advice me what I am doing wrong
Hi Tony,
When I use the onJsAlert with “return true” it breaks the code execution and my webview remains blank.
Do you have any idea how to fix that?
thanks
rafa
If you use the alert hack, don’t forget to call submit(), otherwise it will break execution if “false” is returned.
this is the code to handle the alert hacks:
@Override
public boolean onJsConfirm(final WebView view, final String url, final String message,
final JsResult result) {
result.confirm();
return true;
}
How to i call the activity through the javascript in phonegap,It is possible or Not
So does this only happen in the virtual device then? i.e. no phones are seeing this?
I have just implemented fully synchronous workaround using onJsPrompt hook. On the javascript side it needs some more code, such as
function callApiWorkaround(name, args){
if(!args) args = [];
var ret = prompt(g_api_signature + stringify({method: name, params: args}));
if(ret && typeof ret == ‘string’){
ret = parse(ret);
return ret.result;
}
return ret;
}
function api_trace(msg, callee){
var method = ‘trace’;
if(api)
return api[method](msg, callee);
return callApiWorkaround(method, [msg, callee]);
}
Jason,
Your article has been helpful to many of us doing android development, thank you.
I have a question i’m sure you can answer, but has yet to be answered as far as i can see. Using your solution, how do we pull the webview’s source code without the javascript interface?
Thanks Jason and ppl in comments,
We’ve been using Appcellerator Titanium and got hit by this bug. Your blog has helped us patch the Titanium mobile sdk with a workaround. Because we are just displaying simple webpages and need no Ti interaction, I just commented out the TiWebViewBinding.java code
webView.addJavascriptInterface(appBinding, “TiApp”);
webView.addJavascriptInterface(apiBinding, “TiAPI”);
//webView.addJavascriptInterface(new TiReturn(), “_TiReturn”);
Hello Jason,
Thank you for the clarification about this. Though I have just some questions regarding this?
Is the bug reproducible on real devices? or it is just on emulator?
Please reply. Your response is very much appreciated.
Hey Jason,
Thanks for providing the building blocks for my fix. I’ve taken your solution and improved on it greatly.
There’s just far too much code to put into this comment so if you don’t mind I’ll just link to it.
Details: http://twigstechtips.blogspot.com/2013/09/android-webviewaddjavascriptinterface.html
Source: https://github.com/twig/twigstechtips-snippets/blob/master/GingerbreadJSFixExample.java
Key points are:
– Applies to all versions of Gingerbread (2.3.x)
– Calls from JS to Android are now synchronous
– No longer have to map out interface methods manually
– Fixed possibility of string separators breaking code
– Much easier to change JS signature and interface names
If you prefer elegant fix, that use decorator class, without need to change your code logic. Try this:
https://github.com/biosonic/Gingerbread-addJavascriptInterface/blob/master/README.md