I've been working on a problem with doing a synchronous call to JavaScript in a WebView
(with a return value) and trying to narrow down the where and why of why it's not working. It seems to be that the WebView
thread is blocking while the main thread is waiting for a response from it -- which shouldn't be the case since theWebView
runs on a separate thread.
I've put together this small sample that demonstrates it (I hope) fairly clearly:
main.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:weightSum="1"> <WebView android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+id/webView"/> </LinearLayout>
MyActivity.java:
package com.example.myapp; import android.app.Activity; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.JavascriptInterface; import android.webkit.WebViewClient; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class MyActivity extends Activity { public final static String TAG = "MyActivity"; private WebView webView; private JSInterface JS; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); webView = (WebView)findViewById(R.id.webView); JS = new JSInterface(); webView.addJavascriptInterface(JS, JS.getInterfaceName()); WebSettings settings = webView.getSettings(); settings.setJavaScriptEnabled(true); webView.setWebViewClient(new WebViewClient() { public void onPageFinished(WebView view, String url) { Log.d(TAG, JS.getEval("test()")); } }); webView.loadData("<script>function test() {JSInterface.log(\"returning Success\"); return 'Success';}</script>Test", "text/html", "UTF-8"); } private class JSInterface { private static final String TAG = "JSInterface"; private final String interfaceName = "JSInterface"; private CountDownLatch latch; private String returnValue; public JSInterface() { } public String getInterfaceName() { return interfaceName; } // JS-side functions can call JSInterface.log() to log to logcat @JavascriptInterface public void log(String str) { // log() gets called from Javascript Log.i(TAG, str); } // JS-side functions will indirectly call setValue() via getEval()'s try block, below @JavascriptInterface public void setValue(String value) { // setValue() receives the value from Javascript Log.d(TAG, "setValue(): " + value); returnValue = value; latch.countDown(); } // getEval() is for when you need to evaluate JS code and get the return value back public String getEval(String js) { Log.d(TAG, "getEval(): " + js); returnValue = null; latch = new CountDownLatch(1); final String code = interfaceName + ".setValue(function(){try{return " + js + "+\"\";}catch(js_eval_err){return '';}}());"; Log.d(TAG, "getEval(): " + code); // It doesn't actually matter which one we use; neither works: if (Build.VERSION.SDK_INT >= 19) webView.evaluateJavascript(code, null); else webView.loadUrl("javascript:" + code); // The problem is that latch.await() appears to block, not allowing the JavaBridge // thread to run -- i.e., to call setValue() and therefore latch.countDown() -- // so latch.await() always runs until it times out and getEval() returns "" try { // Set a 4 second timeout for the worst/longest possible case latch.await(4, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.e(TAG, "InterruptedException"); } if (returnValue == null) { Log.i(TAG, "getEval(): Timed out waiting for response"); returnValue = ""; } Log.d(TAG, "getEval() = " + returnValue); return returnValue; } // eval() is for when you need to run some JS code and don't care about any return value public void eval(String js) { // No return value Log.d(TAG, "eval(): " + js); if (Build.VERSION.SDK_INT >= 19) webView.evaluateJavascript(js, null); else webView.loadUrl("javascript:" + js); } } }
When running, the following results:
Emulator Nexus 5 API 23: 05-25 13:34:46.222 16073-16073/com.example.myapp D/JSInterface: getEval(): test() 05-25 13:34:50.224 16073-16073/com.example.myapp I/JSInterface: getEval(): Timed out waiting for response 05-25 13:34:50.224 16073-16073/com.example.myapp D/JSInterface: getEval() = 05-25 13:34:50.225 16073-16073/com.example.myapp I/Choreographer: Skipped 239 frames! The application may be doing too much work on its main thread. 05-25 13:34:50.235 16073-16150/com.example.myapp I/JSInterface: returning Success 05-25 13:34:50.237 16073-16150/com.example.myapp D/JSInterface: setValue(): Success
(16073 is 'main'; 16150 is 'JavaBridge')
As you can see, the main thread times out waiting for theWebView
to call setValue()
, which it doesn't until latch.await()
has timed out and main thread execution has continued.
Interestingly, trying with an earlier API level:
Emulator Nexus S API 14: 05-25 13:37:15.225 19458-19458/com.example.myapp D/JSInterface: getEval(): test() 05-25 13:37:15.235 19458-19543/com.example.myapp I/JSInterface: returning Success 05-25 13:37:15.235 19458-19543/com.example.myapp D/JSInterface: setValue(): Success 05-25 13:37:15.235 19458-19458/com.example.myapp D/JSInterface: getEval() = Success 05-25 13:37:15.235 19458-19458/com.example.myapp D/MyActivity: Success
(19458 is 'main'; 19543 is 'JavaBridge')
Things work correctly in sequence, with getEval()
causing the WebView
to call setValue()
, which then exits latch.await()
before it times out (as you'd expect/hope).
(I've also tried with an even earlier API level, but things crash out due to what may be, as I understand it, an emulator-only bug in 2.3.3 that never got fixed.)
So I'm at a bit of a loss. In digging around, this seems like the correct approach to doing things. It certainly seems like the correct approach because it works properly on API level 14. But then it's failing on later versions — and I've tested on 5.1 and 6.0 without success.
Look more about migration WebView with Android 4.4. See description on Android Docs I think you need to use another method for funning your JS action.
For example, base on that doc - Running JS Async Asynchronously evaluates JavaScript in the context of the currently displayed page. If non-null, |resultCallback| will be invoked with any result returned from that execution. This method must be called on the UI thread and the callback will be made on the UI thread.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With