89

I want to get a return value from Javascript in Android. I can do it with the iPhone, but I can't with Android. I used loadUrl, but it returned void instead of an object. Can anybody help me?

1

10 Answers 10

88

Same as Keith but shorter answer

webView.addJavascriptInterface(this, "android");
webView.loadUrl("javascript:android.onData(functionThatReturnsSomething)");

And implement the function

@JavascriptInterface
public void onData(String value) {
   //.. do something with the data
}

Don't forget to remove the onData from proguard list (if you have enabled proguard)

Sign up to request clarification or add additional context in comments.

4 Comments

can you elaborate on the proguard part?
@eugene if you don't know what it is you probably don't use it. Anyway its something to enable which makes reverse engineering your app harder
Thanks. I know I have proguard-project.txt in the project root but no more than that. I'm asking because your solution works but I get SIGSEGV. I am evaluating document.getElementById("my-div").scrollTop to get scroll position from SwipeRefreshLayout::canChildScrollUp(). I suspect it might be due to the call gets called too many times in short period. or proguard which I suspect because I just don't know what it is..
Not necessary to use loadUrl. What is necessary is that you define webView.addJavascriptInterface(this, "android"); before calling webview.loadUrl() method. Also, you can directly call android.onData(result) from anywhere inside JavaScript. Even inside an async function that doesn't return anything.
64

Here's a hack on how you can accomplish it:

Add this Client to your WebView:

final class MyWebChromeClient extends WebChromeClient {
        @Override
        public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
            Log.d("LogTag", message);
            result.confirm();
            return true;
        }
    }

Now in your javascript call do:

webView.loadUrl("javascript:alert(functionThatReturnsSomething)");

Now in the onJsAlert call "message" will contain the returned value.

5 Comments

This is an extremely valuable hack; especially when you're building an app where you don't have control over the javascript and have to make do with existing functions. Note you can get the same result without intercepting the JS alerts using the public boolean onConsoleMessage(ConsoleMessage consoleMessage) instead of onJsAlert and then in the javascript call you do webView.loadUrl("javascript:console.log(functionThatReturnsSomething)"); - that way you leave the JS alerts alone.
The solution with WebChromeClient.onConsoleMessage seems to be cleaner but be aware that it does not work with some HTC devices. See this question for more info.
Any reason using addJavascriptInterface wouldn't be preferable?
@Keith: You'd require an object into which javascript writes it results, but as you can't touch the existing javascript in this question and the existing js does not write into such object, the js interface method doesn't help here (that's how I understood it).
@RayKoopa Not necessarily true. Although you may not be able to edit the existing javascript within the target webpage, you can still invoke methods onto the object passed into mWebView.addJavascriptInterface(YourObject mYourObject,String yourNameForObject); by calling the function mWebView.loadUrl("javascript:yourNameForObject.yourSetterMethod(valueToSet)")
22

Use addJavascriptInterface() to add a Java object to the Javascript environment. Have your Javascript call a method on that Java object to supply its "return value".

4 Comments

how can i do that in situation like stackoverflow.com/questions/5521191/…
This doesn't help if your js doesn't write into this object and you can't touch the js.
@PacMani: Use loadUrl("javascript:...") (API Level 1-18) or evaluateJavascript() (API Level 19+) to evaluate your own JavaScript in the context of the currently-loaded Web page.
@CommonsWare: Thanks, I got that ;) however, the manager decided that js can be modified over "abusing javascript alerts" (eyeroll), so I got it going with the interface method.
14

On API 19+, the best way to do this is to call evaluateJavascript on your WebView:

webView.evaluateJavascript("foo.bar()", new ValueCallback<String>() {
    @Override public void onReceiveValue(String value) {
        // value is the result returned by the Javascript as JSON
    }
});

Related answer with more detail: https://stackoverflow.com/a/20377857

Comments

12

Here's what I came up with today. It's thread-safe, reasonably efficient, and allows for synchronous Javascript execution from Java for an Android WebView.

Works in Android 2.2 and up. (Requires commons-lang because I need my code snippets passed to eval() as a Javascript string. You could remove this dependency by wrapping the code not in quotation marks, but in function(){})

First, add this to your Javascript file:

function evalJsForAndroid(evalJs_index, jsString) {
    var evalJs_result = "";
    try {
        evalJs_result = ""+eval(jsString);
    } catch (e) {
        console.log(e);
    }
    androidInterface.processReturnValue(evalJs_index, evalJs_result);
}

Then, add this to your Android activity:

private Handler handler = new Handler();
private final AtomicInteger evalJsIndex = new AtomicInteger(0);
private final Map<Integer, String> jsReturnValues = new HashMap<Integer, String>();
private final Object jsReturnValueLock = new Object();
private WebView webView;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    webView = (WebView) findViewById(R.id.webView);
    webView.addJavascriptInterface(new MyJavascriptInterface(this), "androidInterface");
}

public String evalJs(final String js) {
    final int index = evalJsIndex.incrementAndGet();
    handler.post(new Runnable() {
        public void run() {
            webView.loadUrl("javascript:evalJsForAndroid(" + index + ", " +
                                    "\"" + StringEscapeUtils.escapeEcmaScript(js) + "\")");
        }
    });
    return waitForJsReturnValue(index, 10000);
}

private String waitForJsReturnValue(int index, int waitMs) {
    long start = System.currentTimeMillis();

    while (true) {
        long elapsed = System.currentTimeMillis() - start;
        if (elapsed > waitMs)
            break;
        synchronized (jsReturnValueLock) {
            String value = jsReturnValues.remove(index);
            if (value != null)
                return value;

            long toWait = waitMs - (System.currentTimeMillis() - start);
            if (toWait > 0)
                try {
                    jsReturnValueLock.wait(toWait);
                } catch (InterruptedException e) {
                    break;
                }
            else
                break;
        }
    }
    Log.e("MyActivity", "Giving up; waited " + (waitMs/1000) + "sec for return value " + index);
    return "";
}

private void processJsReturnValue(int index, String value) {
    synchronized (jsReturnValueLock) {
        jsReturnValues.put(index, value);
        jsReturnValueLock.notifyAll();
    }
}

private static class MyJavascriptInterface {
    private MyActivity activity;

    public MyJavascriptInterface(MyActivity activity) {
        this.activity = activity;
    }

    // this annotation is required in Jelly Bean and later:
    @JavascriptInterface
    public void processReturnValue(int index, String value) {
        activity.processJsReturnValue(index, value);
    }
}

1 Comment

But if I run evalJS in button.click listener, etc,. waitForJsReturnValue may cause evalJs() to hang up. So webView.loadUrl() inside handler.post() may not have chance to execute because button.click listener didn't returned.
5

The solution that @Felix Khazin suggested works, but there is one key point missing.

The javascript call should be made after the web page in the WebView is loaded. Add this WebViewClient to the WebView, along with the WebChromeClient.

Full Example:

@Override
public void onCreate(Bundle savedInstanceState) {
    ...
    WebView webView = (WebView) findViewById(R.id.web_view);
    webView.getSettings().setJavaScriptEnabled(true);
    webView.setWebViewClient(new MyWebViewClient());
    webView.setWebChromeClient(new MyWebChromeClient());
    webView.loadUrl("http://example.com");
}

private class MyWebViewClient extends WebViewClient {
    @Override
    public void onPageFinished (WebView view, String url){
        view.loadUrl("javascript:alert(functionThatReturnsSomething())");
    }
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        return false;
    }
}

private class MyWebChromeClient extends WebChromeClient {
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    Log.d("LogTag", message);
        result.confirm();
        return true;
    }
}

Comments

3

Android return value from javascript in Android WebView

As an alternative variant that uses a custom scheme to communicate Android native code <-> HTML/JS code. for example MRAID uses this technic[About]

MainActivity

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            WebView.setWebContentsDebuggingEnabled(true);
        }

        final WebView webview = new CustomWebView(this);
        setContentView(webview);
        webview.loadUrl("file:///android_asset/customPage.html");

        webview.postDelayed(new Runnable() {
            @Override
            public void run() {

                //Android -> JS
                webview.loadUrl("javascript:showToast()");
            }
        }, 1000);
    }
}

CustomWebView

public class CustomWebView extends WebView {
    public CustomWebView(Context context) {
        super(context);

        setup();
    }

    @SuppressLint("SetJavaScriptEnabled")
    private void setup() {
        setWebViewClient(new AdWebViewClient());

        getSettings().setJavaScriptEnabled(true);
    }

    private class AdWebViewClient extends WebViewClient {

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {

            if (url.startsWith("customschema://")) {
                //parse uri
                Toast.makeText(CustomWebView.this.getContext(), "event was received", Toast.LENGTH_SHORT).show();

                return true;
            }
            return false;
        }

    }
}

customPage.html (located in the assets folded)

<!DOCTYPE html>
<html>
<head>
    <title>JavaScript View</title>

    <script type="text/javascript">
        <!--JS -> Android-->
        function showToast() {
            window.location = "customschema://goto/";
        }
    </script>
</head>

<body>
</body>

</html>

Comments

2

To get data from a WebView in Android, you can use the evaluateJavascript() method to execute JavaScript code that retrieves the data from the web page. Here's an example:

WebView webView = findViewById(R.id.webview); webView.getSettings().setJavaScriptEnabled(true); webView.loadUrl("https://example.com");

String js = "(function() { " +
        "return document.getElementById('my_element').innerHTML;" +
        "})()";

webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        view.evaluateJavascript(js, new ValueCallback<String>() {
            @Override
            public void onReceiveValue(String value) {
                // 'value' contains the result of the JavaScript code
                Log.d("WebView", "Data: " + value);
            }
        });
    }
});

In this example, the JavaScript code retrieves the inner HTML of an element with the ID my_element. The evaluateJavascript() method executes the code and passes the result to a ValueCallback that logs the data to the console. You can modify the JavaScript code to retrieve different types of data, such as form input values or attributes of HTML elements. Just make sure to return the data as a string, and parse it in the onReceiveValue() method if necessary.

1 Comment

did you use any generative AI at all in the writing of this answer post?
0

You can do it like this:

[Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)]
public class MainActivity : AppCompatActivity
{
    public WebView web_view;
    public static TextView textView;

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        Xamarin.Essentials.Platform.Init(this, savedInstanceState);

        Window.AddFlags(WindowManagerFlags.Fullscreen);
        Window.ClearFlags(WindowManagerFlags.ForceNotFullscreen);

        // Set our view from the "main" layout resource
        SetContentView (Resource.Layout.main);

        web_view = FindViewById<WebView>(Resource.Id.webView);
        textView = FindViewById<TextView>(Resource.Id.textView);

        web_view.Settings.JavaScriptEnabled = true;
        web_view.SetWebViewClient(new SMOSWebViewClient());
        web_view.LoadUrl("https://stns.egyptair.com");
    }

    public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
    {
        Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

        base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

public class SMOSWebViewClient : WebViewClient
{
    public override bool ShouldOverrideUrlLoading(WebView view, IWebResourceRequest request)
    {
        view.LoadUrl(request.Url.ToString());
        return false;
    }

    public override void OnPageFinished(WebView view, string url)
    {
        view.EvaluateJavascript("document.getElementsByClassName('notf')[0].innerHTML;", new JavascriptResult());
    }
}

public class JavascriptResult : Java.Lang.Object, IValueCallback
{
    public string Result;

    public void OnReceiveValue(Java.Lang.Object result)
    {
        string json = ((Java.Lang.String)result).ToString();
        Result = json;
        MainActivity.textView.Text = Result.Replace("\"", string.Empty);
    }
}

Comments

0

If you want to get the value in a synchronous way, you have to know that the webView.evaluateJavascript() runs asynchronously but you can use a simple semaphore to wait for the execution.

Steps:

  1. Create a javascript interface (basically a class with a method that is going to be called from the javascript we want to evaluate.

     public class MyInterface 
     {
         private String value;
         private Semaphore semaphore;
    
         public MyInterface(Semaphore semaphore) {
             this.semaphore = semaphore;
         }
    
         public String getValue() {
             return value;
         }
    
         @JavascriptInterface
         public void setValue(String value) {
             this.value = value;
             //important release the semaphore after the execution
             semaphore.release();
         }
     }
    
  2. Add the javascript interface to the Android web view

     Semaphore mySemaphore = new Semaphore(0);
     MyInterface myJsInterface = new MyInterface(mySemaphore);
     webView.addJavascriptInterface(myJsInterface, "jsInterface");
    
  3. Execute the javascript and await the execution

     webView.evaluateJavascript("var myValue='this works';jsInterface.setValue(myValue);", null);
     //await the execution
     mySemaphore.acquire();
     //the interface now have the value after the execution
     myJsInterface.getValue();
    

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.