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 the WebView 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 the WebView 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.

-- 
You received this message because you are subscribed to the Google Groups 
"Android Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/android-developers.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/android-developers/ba97f3b0-c3e5-4925-a3f6-69877e7b86c8%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to