So, I'm pleased to say that my problem has been solved. Basically, it's a known bug in Gingerbread, and is present on my 2.3.4 device. After some head scratching, I found this workaround concocted by Jason Shah at PhoneGap. The real kudos for this goes to him as my solution is a slightly modified version of the code in that post.
The WebView
In my onLoad method, I call the following function.
private void configureWebView() {
try {
if (Build.VERSION.RELEASE.startsWith("2.3")) {
javascriptInterfaceBroken = true;
}
} catch (Exception e) {
// Ignore, and assume user javascript interface is working correctly.
}
threadView = (WebView) findViewById(R.id.webViewThread);
threadView.setWebViewClient(new ThreadViewClient());
Log.d(APP_NAME, "Interface Broken? " + javascriptInterfaceBroken.toString());
// Add javascript interface only if it's not broken
iface = new JavaScriptInterface(this);
if (!javascriptInterfaceBroken) {
threadView.addJavascriptInterface(new JavaScriptInterface(this), "Android");
}
}
There are several things going on here.
In contrast with the PhoneGap method, I'm using a startsWith
comparison against the version string. This is because Build.VERSION.RELEASE is 2.3.4 on my reference device. Rather than test against all releases in the 2.3 series, I'm comfortable painting all devices with one brushstroke.
javascriptInterface is a bool
initialized to false
. JavaScriptInterface, instantiated as iface, is the class that normally handles JS events in my WebView.
ThreadViewClient is the meat and potatoes of my implementation. It's where all the logic for handling the workaround occurs.
The WebViewClient
In the class ThreadViewClient (which extends WebViewClient), I first account for the fact that the js handler that Android normally attaches isn't here. This means that, if I want to use the same javascript calls from within my WebView, I need to duplicate the interface. This is accomplished by inserting custom handlers into the content of your website once it has loaded...
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (javascriptInterfaceBroken) {
final String handleGingerbreadStupidity =
"javascript:function shortSignature(id) { window.location='http://MyHandler:shortSignature:'+id; }; "
+ "javascript: function longSignature(text, username, forumnumber,threadnumber,pagenumber,postid) { var sep='[MyHandler]';"
+ "window.location='http://MyHandler:longSignature:' + encodeURIComponent(text + sep + username + sep + forumnumber + sep + threadnumber + sep + pagenumber + sep + postid);};"
+ "javascript: function handler() { this.shortSignature = shortSignature; this.longSignature = longSignature;}; "
+ "javascript: var Android = new handler();";
view.loadUrl(handleGingerbreadStupidity);
}
}
There's a lot to process there. In the javascript, I define an object handler
that contains the functions that map to my js interface. An instance of it is then bound to "Android", which is the same interface name as that used by non-2.3 implementation. This allows for re-use of the code rendered within your webview content.
The functions take advantage of the fact that Android allows one to intercept all navigation that occurs within a WebView. In order to communicate with the outside program, they alter the window location to one with a special signature. I'll get into this in a bit.
Another thing I'm doing is concatenating the parameters of functions with more than one parameter. This allows me to reduce the code complexity within the location handler.
The location handler is also placed in ThreadViewClient...
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Method sMethod = null;
Log.d(APP_NAME, "URL LOADING");
if (javascriptInterfaceBroken) {
if (url.contains("MyHandler")) {
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();
Log.d(APP_NAME, "Handler: " + function + " " + parameter);
try {
if (function.equals("shortSignature")) {
iface.shortSignature(parameter);
} else if (function.equals("longSignature")) {
iface.longSignature(parameter);
} else {
if (sMethod == null) {
sMethod = iface.getClass().getMethod(function, new Class[] { String.class });
}
sMethod.invoke(iface, parameter);
}
}
//Catch & handle SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException
return true;
}
}
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
return true;
}
Here I am intercepting all URL load events that occur in the WebView. If the destination URL contains a magic string, the app attempts to parse it to extract out the method call. Rather than using the tokenizer to extract the individual parameters, I'm passing it to version of my longSignature
method that can parse and handle it. This is detailed in the final part of this post.
If, by the time it has exited the "javascriptInterfaceBroken" block, execution has not be returned to the caller, this method treats the URL loading action as a normal link clicked event. In the case of my application I don't want to use the WebView for that, so I pass it off to the operating system via the ACTION_VIEW intent.
This is very similar to the implementation on Jason's blog. However I am bypassing reflection for the most part. I was attempting to use the method in the block with reflection to handle all of my bound functions, but due to my JavaScriptInterface being a nested class I was unable to look into it from another. However, since I defined the interface within the main Activity scope, its methods can be called directly.
Handling Concatenated Parameters
Finally, in my JavaScriptInterface, I created a handler to deal with the case of a concatenated parameter...
public void longSignature(String everything) {
try {
everything = URLDecoder.decode(everything, "UTF-8");
} catch (UnsupportedEncodingException e) {
Log.e(APP_NAME, e);
}
final String[] elements = everything.split("\\[MyHandler\\]");
if (elements.length != 6) {
Toast.makeText(getApplicationContext(), "[" + elements.length + "] wrong number of parameters!", Toast.LENGTH_SHORT).show();
}
else {
longSignature(elements[0], elements[1], elements[2], elements[3], elements[4], elements[5]);
}
}
Hooray polymorphism!
And that's my solution! There's a lot of room for improvement, but, for now, this is sufficient. Sorry if some of my conventions have raised your hackles - this is my first Android app and I am unfamiliar with some of the best practices and conventions. Good luck!