¿Cómo obtener el valor de retorno de javascript en WebView de Android?

82

Quiero obtener un valor de retorno de Javascript en Android. Puedo hacerlo con el iPhone, pero no con Android. Usé loadUrl, pero devolvió void en lugar de un objeto. Alguien puede ayudarme?

Cuong Ta
fuente

Respuestas:

79

Igual que Keithpero respuesta más corta

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

E implementar la función

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

No se olvide de quitar el onDatade proguardla lista (si se ha habilitado Proguard)

Kirill Kulakov
fuente
1
¿Puedes dar más detalles sobre la proguardparte?
eugene
@eugene, si no sabes qué es, probablemente no lo uses. De todos modos es algo que permita que hace la ingeniería inversa de su aplicación más difícil
Kirill Kulakov
1
Gracias. Sé que tengo proguard-project.txt en la raíz del proyecto, pero no más que eso. Lo pregunto porque su solución funciona pero obtengo SIGSEGV. Estoy evaluando document.getElementById("my-div").scrollTopobtener la posición de desplazamiento desde SwipeRefreshLayout::canChildScrollUp(). Sospecho que podría deberse a que la llamada se llama demasiadas veces en un período corto. o proguardlo que sospecho porque simplemente no sé qué es ..
eugene
Esta debería ser la respuesta aceptada en lugar de todos los
trucos
64

Aquí hay un truco sobre cómo puede lograrlo:

Agregue este cliente a su 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;
        }
    }

Ahora en su llamada javascript haga:

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

Ahora, en la llamada onJsAlert, el "mensaje" contendrá el valor devuelto.

Felix Khazin
fuente
17
Este es un truco extremadamente valioso; especialmente cuando está creando una aplicación en la que no tiene control sobre javascript y tiene que conformarse con las funciones existentes. Tenga en cuenta que puede obtener el mismo resultado sin interceptar las alertas de JS usando el en public boolean onConsoleMessage(ConsoleMessage consoleMessage)lugar de onJsAlerty luego en la llamada de JavaScript que hace webView.loadUrl("javascript:console.log(functionThatReturnsSomething)");; de esa manera, deja las alertas de JS en paz.
Mick Byrne
La solución WebChromeClient.onConsoleMessageparece ser más limpia, pero tenga en cuenta que no funciona con algunos dispositivos HTC. Consulte esta pregunta para obtener más información.
Piotr
3
¿Alguna razón no sería preferible usar addJavascriptInterface?
Keith
@Keith: Necesitaría un objeto en el que javascript escriba los resultados, pero como no puede tocar el javascript existente en esta pregunta y el js existente no escribe en dicho objeto, el método de interfaz js no ayuda aquí ( así lo entendí).
Ray
1
@RayKoopa No es necesariamente cierto. Aunque es posible que no pueda editar el javascript existente dentro de la página web de destino, aún puede invocar métodos en el objeto pasado mWebView.addJavascriptInterface(YourObject mYourObject,String yourNameForObject);llamando a la funciónmWebView.loadUrl("javascript:yourNameForObject.yourSetterMethod(valueToSet)")
Chris - Jr
20

Úselo addJavascriptInterface()para agregar un objeto Java al entorno Javascript. Haga que su Javascript llame a un método en ese objeto Java para proporcionar su "valor de retorno".

CommonsWare
fuente
¿Cómo puedo hacer eso en una situación como stackoverflow.com/questions/5521191/…
vnshetty
Esto no ayuda si su js no escribe en este objeto y no puede tocar el js.
Ray
7
@PacMani: Use loadUrl("javascript:...")(API nivel 1-18) o evaluateJavascript()(API nivel 19+) para evaluar su propio JavaScript en el contexto de la página web cargada actualmente.
CommonsWare
@CommonsWare: Gracias, lo entendí;) sin embargo, el administrador decidió que js se puede modificar en lugar de "abusar de las alertas de JavaScript" (eyeroll), así que lo puse en marcha con el método de interfaz.
Ray
12

Esto es lo que se me ocurrió hoy. Es seguro para subprocesos, razonablemente eficiente y permite la ejecución síncrona de Javascript desde Java para un WebView de Android .

Funciona en Android 2.2 y versiones posteriores. (Requiere commons-lang porque necesito que mis fragmentos de código pasen a eval () como una cadena de JavaScript. Puede eliminar esta dependencia envolviendo el código no entre comillas, sino en function(){})

Primero, agregue esto a su archivo Javascript:

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

Luego, agregue esto a su actividad de Android:

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);
    }
}
Keith
fuente
2
Gracias por el código anterior, lo adopté en mi proyecto de Android. Sin embargo, hay una trampa, resultante de un mal diseño de la API de Java. La línea: jsReturnValueLock.wait (waitMs - (System.currentTimeMillis () - start)); El valor del argumento de la función wait () a veces puede suceder que se evalúe a 0. Y esto significa "esperar infinitamente" - obtenía errores ocasionales de "no respuesta". Lo resolví probando: if (transcurrido> = waitMS) break; y no volver a llamar a System.currentTimeMillis () a continuación, pero utilizando el valor transcurrido. Lo mejor sería editar y corregir el código anterior.
Gregko
¡Vaya, muchas gracias! Yo también estaba viendo eso colgado y nunca supe de dónde venía.
Keith
1
Pero si ejecuto evalJS en button.click oyente, etc. waitForJsReturnValue puede hacer que evalJs () cuelgue. Así que webView.loadUrl () dentro de handler.post () puede no tener la oportunidad de ejecutarse porque el oyente button.click no regresó.
victorwoo
Pero, ¿cómo envolver el código no entre comillas, sino en function(){}? ¿Cómo colocar jsString en {}?
victorwoo
7

En API 19+, la mejor manera de hacer esto es llamar evaluateJavascript en su vista web:

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

Respuesta relacionada con más detalles: https://stackoverflow.com/a/20377857

David
fuente
5

La solución que sugirió @Felix Khazin funciona, pero falta un punto clave.

La llamada javascript debe realizarse después de que WebViewse cargue la página web en el . Agregue esto WebViewCliental WebView, junto con el WebChromeClient.

Ejemplo completo:

@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;
    }
}
Mitko Katsev
fuente
1

Como variante alternativa que usa un esquema personalizado:

Actividad principal

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 (ubicado en los activos plegados)

<!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>
yoAlex5
fuente
0

Puedes hacerlo así:

[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);
    }
}
Riad Sherif
fuente