Handling Android 2.3 WebView’s broken AddJavascriptInterface

(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.